From e708bc28db24995b9a524364a8267cac32b60627 Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 17:57:10 +0300 Subject: [PATCH 1/9] sk-queue: don't drop deferred packets with zero ucred pid A queued skb without attached credentials surfaces at recvmsg time as SCM_CREDENTIALS with pid 0 (and overflow uid/gid) when the receiver has SO_PASSCRED set. The deferred-write path in sk_queue_post_actions() searches the process tree for the ucred pid and drops the packet when no match is found -- which is always the case for pid 0, so such packets are silently lost on dump. Write these packets as is instead: there is no pid to translate, and on restore send_one_pkt() already skips the SCM_CREDENTIALS spoof for a zero pid, so the packet is re-queued without a forged sender. Note the remaining fidelity limit: if the receiver's SO_PASS* options happen to be restored before the packet is re-sent, the kernel attaches the restorer task's credentials to the skb. This is still strictly better than dropping the packet altogether. Signed-off-by: Ahmed Elaidy --- criu/sk-queue.c | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/criu/sk-queue.c b/criu/sk-queue.c index f18ee91a3..1ee63d522 100644 --- a/criu/sk-queue.c +++ b/criu/sk-queue.c @@ -172,22 +172,29 @@ int sk_queue_post_actions(void) struct pstree_item *item, *found = NULL; SkUcredEntry *ue = pkt->entry->ucred; - for_each_pstree_item(item) { - if (item->pid->real == ue->pid) { - found = item; - break; + /* + * A zero pid means the kernel attached no struct pid + * to the skb (credless sender), there is nothing to + * fix up, write the packet as is. + */ + if (ue->pid != 0) { + for_each_pstree_item(item) { + if (item->pid->real == ue->pid) { + found = item; + break; + } } - } - if (!found) { - pr_warn("ucred: Can't find process with pid %d, ignoring packet\n", - ue->pid); - goto next; - } + if (!found) { + pr_warn("ucred: Can't find process with pid %d, ignoring packet\n", + ue->pid); + goto next; + } - pr_debug("ucred: Fixup ucred pids %d -> %d\n", - ue->pid, vpid(item)); - ue->pid = vpid(item); + pr_debug("ucred: Fixup ucred pids %d -> %d\n", + ue->pid, vpid(item)); + ue->pid = vpid(item); + } ret = pb_write_one(img, pkt->entry, PB_SK_QUEUES); if (ret < 0) { From b1829d1e1c352873db6c1486939be6b2711ace39 Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 17:59:26 +0300 Subject: [PATCH 2/9] sockets: add fallback definitions for SO_PASSPIDFD, SO_PEERPIDFD and SCM_PIDFD These constants were introduced in kernel 6.5 ("scm: add SO_PASSPIDFD and SCM_PIDFD" and "net: core: add getsockopt SO_PEERPIDFD") and may be missing from older toolchain headers. Define them if not provided, next to the existing SO_BUF_LOCK fallback. Signed-off-by: Ahmed Elaidy --- criu/include/sockets.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/criu/include/sockets.h b/criu/include/sockets.h index 764cb93b2..be6734c52 100644 --- a/criu/include/sockets.h +++ b/criu/include/sockets.h @@ -129,4 +129,16 @@ extern const char *socket_proto_name(unsigned int proto, char *nm, size_t size); #define SO_BUF_LOCK 72 #endif +#ifndef SO_PASSPIDFD +#define SO_PASSPIDFD 76 +#endif + +#ifndef SO_PEERPIDFD +#define SO_PEERPIDFD 77 +#endif + +#ifndef SCM_PIDFD +#define SCM_PIDFD 0x04 +#endif + #endif /* __CR_SOCKETS_H__ */ From 702624dcb15d79cfbe9d1201aab1f4b3e88b180f Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 18:04:18 +0300 Subject: [PATCH 3/9] kerndat/cr-check: detect SO_PASSPIDFD support SO_PASSPIDFD is available since kernel 6.5. Probe it with getsockopt on an AF_UNIX socket, treating ENOPROTOOPT as "not supported", the same way SO_BUF_LOCK is detected. Expose the result as the "so_passpidfd" feature so tests can be gated on it with criu check. Signed-off-by: Ahmed Elaidy --- criu/cr-check.c | 12 ++++++++++++ criu/include/kerndat.h | 1 + criu/kerndat.c | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/criu/cr-check.c b/criu/cr-check.c index 67a49ee28..881ce47b1 100644 --- a/criu/cr-check.c +++ b/criu/cr-check.c @@ -1360,6 +1360,16 @@ static int check_sockopt_buf_lock(void) return 0; } +static int check_so_passpidfd(void) +{ + if (!kdat.has_so_passpidfd) { + pr_warn("SO_PASSPIDFD is not supported\n"); + return -1; + } + + return 0; +} + static int check_move_mount_set_group(void) { if (!kdat.has_move_mount_set_group) @@ -1721,6 +1731,7 @@ int cr_check(void) ret |= check_overlayfs_maps(); ret |= check_timer_cr_ids(); ret |= check_pagemap_scan_guard_pages(); + ret |= check_so_passpidfd(); if (kdat.lsm == LSMTYPE__APPARMOR) ret |= check_apparmor_stacking(); @@ -1870,6 +1881,7 @@ static struct feature_list feature_list[] = { { "overlayfs_maps", check_overlayfs_maps }, { "pagemap_scan_guard_pages", check_pagemap_scan_guard_pages }, { "binfmt_misc_sandboxing", check_binfmt_misc_sandboxing }, + { "so_passpidfd", check_so_passpidfd }, { "compress", check_compress }, { NULL, NULL }, }; diff --git a/criu/include/kerndat.h b/criu/include/kerndat.h index 27bdfe557..f23b5a609 100644 --- a/criu/include/kerndat.h +++ b/criu/include/kerndat.h @@ -93,6 +93,7 @@ struct kerndat_s { bool has_madv_guard; bool has_pagemap_scan_guard_pages; bool has_binfmt_misc_sandboxing; + bool has_so_passpidfd; }; extern struct kerndat_s kdat; diff --git a/criu/kerndat.c b/criu/kerndat.c index 1e4863190..b9b7f0433 100644 --- a/criu/kerndat.c +++ b/criu/kerndat.c @@ -1072,6 +1072,35 @@ err: return exit_code; } +static int kerndat_has_so_passpidfd(void) +{ + int exit_code = -1; + socklen_t len; + int val; + int sock; + + sock = socket(AF_UNIX, SOCK_DGRAM, 0); + if (sock < 0) { + pr_perror("Unable to create a unix socket"); + return -1; + } + + len = sizeof(val); + if (getsockopt(sock, SOL_SOCKET, SO_PASSPIDFD, &val, &len)) { + if (errno != ENOPROTOOPT) { + pr_perror("Unable to get SO_PASSPIDFD with getsockopt"); + goto err; + } + kdat.has_so_passpidfd = false; + } else + kdat.has_so_passpidfd = true; + + exit_code = 0; +err: + close(sock); + return exit_code; +} + static int kerndat_has_move_mount_set_group(void) { char tmpdir[] = "/tmp/.criu.move_mount_set_group.XXXXXX"; @@ -2090,6 +2119,10 @@ int kerndat_init(void) pr_err("kerndat_sockopt_buf_lock failed when initializing kerndat.\n"); ret = -1; } + if (!ret && kerndat_has_so_passpidfd()) { + pr_err("kerndat_has_so_passpidfd failed when initializing kerndat.\n"); + ret = -1; + } if (!ret && kerndat_has_openat2()) { pr_err("kerndat_has_openat2 failed when initializing kerndat.\n"); ret = -1; From a97f5c39354049051b9289cbd6621430b5a4b466 Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 18:05:56 +0300 Subject: [PATCH 4/9] sockets: dump and restore SO_PASSPIDFD Save the SO_PASSPIDFD state of unix sockets in a new so_passpidfd field of sk_opts_entry and set it back on restore. The option is dumped for AF_UNIX sockets only: since Linux 6.16, commit 7d8d93fdde50 ("net: Restrict SO_PASS{CRED,PIDFD,SEC} to AF_{UNIX,NETLINK,BLUETOOTH}") makes get/setsockopt(SO_PASSPIDFD) fail with EOPNOTSUPP on anything but unix sockets (SO_PASSCRED remains valid on netlink), so probing it on netlink sockets would fail the dump of any task holding one. Dumping is gated on kernel support detected by kerndat. Restoring is attempted whenever the image has the bit set, so migrating to a kernel without SO_PASSPIDFD support fails loudly instead of silently dropping the option. Signed-off-by: Ahmed Elaidy --- criu/sockets.c | 15 +++++++++++++++ images/sk-opts.proto | 2 ++ 2 files changed, 17 insertions(+) diff --git a/criu/sockets.c b/criu/sockets.c index 7778b4688..54cbc327e 100644 --- a/criu/sockets.c +++ b/criu/sockets.c @@ -567,6 +567,10 @@ int restore_socket_opts(int sk, SkOptsEntry *soe) pr_debug("\tset passcred for socket\n"); ret |= restore_opt(sk, SOL_SOCKET, SO_PASSCRED, &val); } + if (soe->has_so_passpidfd && soe->so_passpidfd) { + pr_debug("\tset passpidfd for socket\n"); + ret |= restore_opt(sk, SOL_SOCKET, SO_PASSPIDFD, &val); + } if (soe->has_so_passsec && soe->so_passsec) { pr_debug("\tset passsec for socket\n"); ret |= restore_opt(sk, SOL_SOCKET, SO_PASSSEC, &val); @@ -696,6 +700,17 @@ int dump_socket_opts(int sk, int family, SkOptsEntry *soe) ret |= dump_opt(sk, SOL_SOCKET, SO_PASSSEC, &val); soe->has_so_passsec = true; soe->so_passsec = val ? true : false; + + /* + * Since Linux 6.16 SO_PASSPIDFD is restricted to unix + * sockets and get/setsockopt fails with EOPNOTSUPP on + * netlink ones. + */ + if (family == AF_UNIX && kdat.has_so_passpidfd) { + ret |= dump_opt(sk, SOL_SOCKET, SO_PASSPIDFD, &val); + soe->has_so_passpidfd = true; + soe->so_passpidfd = val ? true : false; + } } ret |= dump_opt(sk, SOL_SOCKET, SO_DONTROUTE, &val); diff --git a/images/sk-opts.proto b/images/sk-opts.proto index 2f9d4e5c3..2913cc77c 100644 --- a/images/sk-opts.proto +++ b/images/sk-opts.proto @@ -36,6 +36,8 @@ message sk_opts_entry { optional uint32 so_linger = 24; optional uint32 so_buf_lock = 25; + + optional bool so_passpidfd = 26; } enum sk_shutdown { From 9a039edc306eff9335a10d6ccd88f67aaf7a960f Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 18:08:27 +0300 Subject: [PATCH 5/9] sk-queue: handle SCM_PIDFD control messages on dump If a dumped unix socket has SO_PASSPIDFD set, the MSG_PEEK loop in dump_sk_queue_packet() receives an SCM_PIDFD cmsg for each queued packet, with a freshly installed pidfd in criu's fd table. Today dump_packet_cmsg() errors out on any unknown cmsg, so such sockets cannot be dumped at all (and the fd would be leaked). The kernel attaches struct pid + creds to the skb at sendmsg time whenever the receiver has SO_PASSCRED or SO_PASSPIDFD set; SCM_PIDFD and SCM_CREDENTIALS are just two recvmsg-time views of the same per-skb state. So instead of converting pidfds back to pids (which would leave uid/gid unknown), normalize: if the socket has SO_PASSPIDFD but not SO_PASSCRED, temporarily enable SO_PASSCRED around the peek loop, the same way SO_PEEK_OFF is toggled there. Every packet then also yields a genuine SCM_CREDENTIALS cmsg with the true skb pid/uid/gid, which the existing ucred dump/restore machinery handles unmodified. The SCM_PIDFD cmsg itself only needs its fd closed. No restore-side changes are needed for the queue: the restored packets are re-sent with a spoofed SCM_CREDENTIALS, which sets the skb pid, and a receiver with SO_PASSPIDFD restored gets a correct pidfd minted from it at recvmsg time. Signed-off-by: Ahmed Elaidy --- criu/sk-queue.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/criu/sk-queue.c b/criu/sk-queue.c index 1ee63d522..37c023429 100644 --- a/criu/sk-queue.c +++ b/criu/sk-queue.c @@ -11,6 +11,7 @@ #include "common/list.h" #include "imgset.h" +#include "kerndat.h" #include "image.h" #include "servicefd.h" #include "cr_options.h" @@ -388,6 +389,20 @@ static int dump_packet_cmsg(struct msghdr *mh, SkPacketEntry *pe, int flags) if (ret < 0) return -1; continue; + } else if (ch->cmsg_len == CMSG_LEN(sizeof(int)) && + ch->cmsg_type == SCM_PIDFD) { + int pidfd = *(int *)CMSG_DATA(ch); + + /* + * The skb pid the pidfd refers to is dumped + * from the SCM_CREDENTIALS cmsg (SO_PASSCRED + * is enabled for the peek when the socket has + * SO_PASSPIDFD), so just close the fd that + * recvmsg installed. + */ + pr_debug("Closing pidfd %d received on queue peek\n", pidfd); + close(pidfd); + continue; } else if (ch->cmsg_type == SCM_TIMESTAMP || ch->cmsg_type == SCM_TIMESTAMPNS || ch->cmsg_type == SCM_TIMESTAMPING) { @@ -482,6 +497,7 @@ int dump_sk_queue(int sock_fd, int sock_id, int flags) { int ret, size, orig_peek_off; int exit_code = -1; + int passcred_set = 0; void *data; socklen_t tmp; @@ -525,6 +541,40 @@ int dump_sk_queue(int sock_fd, int sock_id, int flags) goto err_free; } + /* + * If the socket has SO_PASSPIDFD but not SO_PASSCRED, peeked packets + * carry an SCM_PIDFD cmsg only, which loses the skb uid/gid and needs + * a pidfd -> pid conversion. Both PASS* options are just recvmsg-time + * views of the same pid/creds the kernel attached to the skb at send + * time, so temporarily enable SO_PASSCRED to make every packet yield + * a full SCM_CREDENTIALS cmsg instead. + */ + if (kdat.has_so_passpidfd) { + int passpidfd = 0, passcred = 0; + + tmp = sizeof(passpidfd); + if (getsockopt(sock_fd, SOL_SOCKET, SO_PASSPIDFD, &passpidfd, &tmp)) { + pr_perror("Unable to get SO_PASSPIDFD"); + goto err_set_sock; + } + + tmp = sizeof(passcred); + if (getsockopt(sock_fd, SOL_SOCKET, SO_PASSCRED, &passcred, &tmp)) { + pr_perror("Unable to get SO_PASSCRED"); + goto err_set_sock; + } + + if (passpidfd && !passcred) { + int one = 1; + + if (setsockopt(sock_fd, SOL_SOCKET, SO_PASSCRED, &one, sizeof(one))) { + pr_perror("Unable to set SO_PASSCRED"); + goto err_set_sock; + } + passcred_set = 1; + } + } + while (1) { ret = dump_sk_queue_packet(sock_fd, sock_id, data, size, flags); if (ret == 1) @@ -535,6 +585,14 @@ int dump_sk_queue(int sock_fd, int sock_id, int flags) exit_code = 0; err_set_sock: + if (passcred_set) { + int zero = 0; + + if (setsockopt(sock_fd, SOL_SOCKET, SO_PASSCRED, &zero, sizeof(zero))) { + pr_perror("Unable to drop SO_PASSCRED back"); + exit_code = -1; + } + } /* * Restore original peek offset. */ From b5661dd45e4ef6177a96844073a2ff765d1ea475 Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 18:11:20 +0300 Subject: [PATCH 6/9] zdtm: add sk-unix-dgram-pidfd test for SCM_PIDFD checkpoint/restore The test creates a dgram socketpair, enables SO_PASSPIDFD on the receiver and queues a message with a plain sendmsg, which makes the kernel attach the sender pid to the skb. After checkpoint/restore it verifies that SO_PASSPIDFD is still set and that recvmsg yields an SCM_PIDFD cmsg with a working pidfd: pidfd_send_signal() accepts it and its fdinfo Pid matches the sender. The test is gated on the so_passpidfd feature (kernel >= 6.5). Signed-off-by: Ahmed Elaidy --- test/zdtm/static/Makefile | 1 + test/zdtm/static/sk-unix-dgram-pidfd.c | 146 ++++++++++++++++++++++ test/zdtm/static/sk-unix-dgram-pidfd.desc | 1 + 3 files changed, 148 insertions(+) create mode 100644 test/zdtm/static/sk-unix-dgram-pidfd.c create mode 100644 test/zdtm/static/sk-unix-dgram-pidfd.desc diff --git a/test/zdtm/static/Makefile b/test/zdtm/static/Makefile index 83cc5bec0..0d284f4f9 100644 --- a/test/zdtm/static/Makefile +++ b/test/zdtm/static/Makefile @@ -488,6 +488,7 @@ TST_DIR = \ sk-unix01 \ sk-unix01-seqpacket \ sk-unix-dgram-cred \ + sk-unix-dgram-pidfd \ sk-unix-dgram-ghost \ unsupported_children_collision \ shared_slave_mount_children \ diff --git a/test/zdtm/static/sk-unix-dgram-pidfd.c b/test/zdtm/static/sk-unix-dgram-pidfd.c new file mode 100644 index 000000000..40109a778 --- /dev/null +++ b/test/zdtm/static/sk-unix-dgram-pidfd.c @@ -0,0 +1,146 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "zdtmtst.h" + +#ifndef SO_PASSPIDFD +#define SO_PASSPIDFD 76 +#endif + +#ifndef SCM_PIDFD +#define SCM_PIDFD 0x04 +#endif + +const char *test_doc = "Test SCM_PIDFD checkpoint/restore in unix socket queue\n"; +const char *test_author = "Ahmed Elaidy "; + +char *dirname; +TEST_OPTION(dirname, string, "directory name", 1); + +static int pidfd_send_signal(int pidfd, int sig, siginfo_t *info, unsigned int flags) +{ + return syscall(__NR_pidfd_send_signal, pidfd, sig, info, flags); +} + +static pid_t pidfd_get_pid(int pidfd) +{ + char path[64]; + char line[256]; + pid_t pid = -1; + FILE *f; + + snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", pidfd); + f = fopen(path, "r"); + if (!f) { + pr_perror("fopen %s", path); + return -1; + } + + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "Pid: %d", &pid) == 1) + break; + } + + fclose(f); + return pid; +} + +int main(int argc, char *argv[]) +{ + int sk[2]; + struct msghdr msg = {}; + struct iovec iov; + char buf[64]; + char cmsg_buf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr *cmsg; + int opt = 1; + int pidfd; + pid_t pid; + socklen_t len; + + test_init(argc, argv); + + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sk) < 0) + return pr_perror("socketpair"); + + if (setsockopt(sk[1], SOL_SOCKET, SO_PASSPIDFD, &opt, sizeof(opt)) < 0) + return pr_perror("setsockopt SO_PASSPIDFD"); + + /* + * A plain sendmsg: the kernel attaches the sender pid to the + * queued skb because the receiver has SO_PASSPIDFD set. + */ + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + strcpy(buf, "hello"); + if (sendmsg(sk[0], &msg, 0) < 0) + return pr_perror("sendmsg"); + + test_daemon(); + test_waitsig(); + + /* Check the socket option survived */ + opt = 0; + len = sizeof(opt); + if (getsockopt(sk[1], SOL_SOCKET, SO_PASSPIDFD, &opt, &len) < 0) + return pr_perror("getsockopt SO_PASSPIDFD"); + if (opt != 1) { + fail("SO_PASSPIDFD not restored"); + return 1; + } + + /* Receive and verify the pidfd after restore */ + memset(buf, 0, sizeof(buf)); + memset(cmsg_buf, 0, sizeof(cmsg_buf)); + msg.msg_control = cmsg_buf; + msg.msg_controllen = sizeof(cmsg_buf); + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + + if (recvmsg(sk[1], &msg, 0) < 0) + return pr_perror("recvmsg"); + + cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg) { + fail("no cmsg after restore"); + return 1; + } + + if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_PIDFD) { + fail("wrong cmsg after restore: level %d type %d", + cmsg->cmsg_level, cmsg->cmsg_type); + return 1; + } + + memcpy(&pidfd, CMSG_DATA(cmsg), sizeof(pidfd)); + if (pidfd < 0) { + fail("invalid pidfd %d after restore", pidfd); + return 1; + } + + if (pidfd_send_signal(pidfd, 0, NULL, 0)) + return pr_perror("pidfd_send_signal"); + + pid = pidfd_get_pid(pidfd); + if (pid != getpid()) { + fail("pidfd pid mismatch after restore: %d/%d", pid, getpid()); + return 1; + } + + close(pidfd); + close(sk[0]); + close(sk[1]); + + pass(); + return 0; +} diff --git a/test/zdtm/static/sk-unix-dgram-pidfd.desc b/test/zdtm/static/sk-unix-dgram-pidfd.desc new file mode 100644 index 000000000..8e9526ec4 --- /dev/null +++ b/test/zdtm/static/sk-unix-dgram-pidfd.desc @@ -0,0 +1 @@ +{'flavor': 'h ns uns', 'feature': 'so_passpidfd'} From 41cae31fd32470c778f6a4bdf9c2cb9430f9faa8 Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 18:12:23 +0300 Subject: [PATCH 7/9] zdtm: add sk-unix-dgram-pidfd-cred test for combined SCM cmsgs Same as sk-unix-dgram-pidfd, but with both SO_PASSCRED and SO_PASSPIDFD enabled on the receiver and the message queued with an explicit SCM_CREDENTIALS cmsg. This exercises the dump path where a peeked packet carries both SCM_CREDENTIALS and SCM_PIDFD control messages at once. After checkpoint/restore the test verifies that a single recvmsg delivers both cmsgs and that they agree: creds match what was sent and the pidfd resolves to the same pid as the SCM_CREDENTIALS view. Signed-off-by: Ahmed Elaidy --- test/zdtm/static/Makefile | 1 + test/zdtm/static/sk-unix-dgram-pidfd-cred.c | 162 ++++++++++++++++++ .../zdtm/static/sk-unix-dgram-pidfd-cred.desc | 1 + 3 files changed, 164 insertions(+) create mode 100644 test/zdtm/static/sk-unix-dgram-pidfd-cred.c create mode 100644 test/zdtm/static/sk-unix-dgram-pidfd-cred.desc diff --git a/test/zdtm/static/Makefile b/test/zdtm/static/Makefile index 0d284f4f9..82f47d870 100644 --- a/test/zdtm/static/Makefile +++ b/test/zdtm/static/Makefile @@ -489,6 +489,7 @@ TST_DIR = \ sk-unix01-seqpacket \ sk-unix-dgram-cred \ sk-unix-dgram-pidfd \ + sk-unix-dgram-pidfd-cred \ sk-unix-dgram-ghost \ unsupported_children_collision \ shared_slave_mount_children \ diff --git a/test/zdtm/static/sk-unix-dgram-pidfd-cred.c b/test/zdtm/static/sk-unix-dgram-pidfd-cred.c new file mode 100644 index 000000000..6c19697ba --- /dev/null +++ b/test/zdtm/static/sk-unix-dgram-pidfd-cred.c @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "zdtmtst.h" + +#ifndef SO_PASSPIDFD +#define SO_PASSPIDFD 76 +#endif + +#ifndef SCM_PIDFD +#define SCM_PIDFD 0x04 +#endif + +const char *test_doc = "Test SCM_CREDENTIALS + SCM_PIDFD checkpoint/restore in one unix socket queue\n"; +const char *test_author = "Ahmed Elaidy "; + +char *dirname; +TEST_OPTION(dirname, string, "directory name", 1); + +static int pidfd_send_signal(int pidfd, int sig, siginfo_t *info, unsigned int flags) +{ + return syscall(__NR_pidfd_send_signal, pidfd, sig, info, flags); +} + +static pid_t pidfd_get_pid(int pidfd) +{ + char path[64]; + char line[256]; + pid_t pid = -1; + FILE *f; + + snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", pidfd); + f = fopen(path, "r"); + if (!f) { + pr_perror("fopen %s", path); + return -1; + } + + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "Pid: %d", &pid) == 1) + break; + } + + fclose(f); + return pid; +} + +int main(int argc, char *argv[]) +{ + int sk[2]; + struct msghdr msg = {}; + struct iovec iov; + char buf[64]; + char cmsg_buf[CMSG_SPACE(sizeof(struct ucred)) + CMSG_SPACE(sizeof(int))]; + struct cmsghdr *cmsg; + struct ucred send_cred; + struct ucred *cred = NULL; + int opt = 1; + int pidfd = -1; + pid_t pid; + + test_init(argc, argv); + + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sk) < 0) + return pr_perror("socketpair"); + + if (setsockopt(sk[1], SOL_SOCKET, SO_PASSCRED, &opt, sizeof(opt)) < 0) + return pr_perror("setsockopt SO_PASSCRED"); + + if (setsockopt(sk[1], SOL_SOCKET, SO_PASSPIDFD, &opt, sizeof(opt)) < 0) + return pr_perror("setsockopt SO_PASSPIDFD"); + + /* Send a message with explicit SCM_CREDENTIALS */ + send_cred.pid = getpid(); + send_cred.uid = getuid(); + send_cred.gid = getgid(); + + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + + memset(cmsg_buf, 0, sizeof(cmsg_buf)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf; + msg.msg_controllen = CMSG_SPACE(sizeof(struct ucred)); + + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_CREDENTIALS; + cmsg->cmsg_len = CMSG_LEN(sizeof(struct ucred)); + memcpy(CMSG_DATA(cmsg), &send_cred, sizeof(struct ucred)); + + strcpy(buf, "hello"); + if (sendmsg(sk[0], &msg, 0) < 0) + return pr_perror("sendmsg"); + + test_daemon(); + test_waitsig(); + + /* Both cmsgs must arrive in one recvmsg after restore */ + memset(buf, 0, sizeof(buf)); + memset(cmsg_buf, 0, sizeof(cmsg_buf)); + msg.msg_control = cmsg_buf; + msg.msg_controllen = sizeof(cmsg_buf); + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + + if (recvmsg(sk[1], &msg, 0) < 0) + return pr_perror("recvmsg"); + + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET) + continue; + if (cmsg->cmsg_type == SCM_CREDENTIALS) + cred = (struct ucred *)CMSG_DATA(cmsg); + else if (cmsg->cmsg_type == SCM_PIDFD) + memcpy(&pidfd, CMSG_DATA(cmsg), sizeof(pidfd)); + } + + if (!cred) { + fail("no SCM_CREDENTIALS after restore"); + return 1; + } + + if (pidfd < 0) { + fail("no SCM_PIDFD after restore"); + return 1; + } + + if (cred->uid != send_cred.uid || cred->gid != send_cred.gid) { + fail("credentials mismatch after restore: " + "uid %d/%d gid %d/%d", + cred->uid, send_cred.uid, + cred->gid, send_cred.gid); + return 1; + } + + if (pidfd_send_signal(pidfd, 0, NULL, 0)) + return pr_perror("pidfd_send_signal"); + + /* The pidfd and the creds are two views of the same skb pid */ + pid = pidfd_get_pid(pidfd); + if (pid != cred->pid || pid != getpid()) { + fail("pid mismatch after restore: pidfd %d cred %d self %d", + pid, cred->pid, getpid()); + return 1; + } + + close(pidfd); + close(sk[0]); + close(sk[1]); + + pass(); + return 0; +} diff --git a/test/zdtm/static/sk-unix-dgram-pidfd-cred.desc b/test/zdtm/static/sk-unix-dgram-pidfd-cred.desc new file mode 100644 index 000000000..8e9526ec4 --- /dev/null +++ b/test/zdtm/static/sk-unix-dgram-pidfd-cred.desc @@ -0,0 +1 @@ +{'flavor': 'h ns uns', 'feature': 'so_passpidfd'} From f0b919dc9b446af09db751fb5b4a4571c7bff1fb Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 18:45:51 +0300 Subject: [PATCH 8/9] sk-queue: C/R packets of dead senders on SO_PASSPIDFD sockets A packet queued to a SO_PASSPIDFD socket whose sender died before the dump is currently dropped in sk_queue_post_actions(), because the skb pid can't be translated to a virtual one. But dropping it isn't necessary for pidfd semantics: the struct pid referenced by the skb is already dead, so the pidfd the receiver would mint from it is stale both before and after C/R -- pidfd_send_signal() fails with ESRCH and fdinfo shows Pid: -1. The exact pid number is not observable through a stale pidfd. Keep such packets instead: mark them with a new 'stale' flag in sk_ucred_entry (the dump side knows the receiver has SO_PASSPIDFD via a new SK_QUEUE_ALLOW_STALE_PID flag). On restore, send_one_pkt() forks a short-lived helper process, attaches its pid via the spoofed SCM_CREDENTIALS, and kills the helper right after sendmsg(). The refcounted struct pid persists in the skb, so the receiver gets a correctly stale pidfd at recvmsg() time. The helper machinery is the same create_tmp_process()/kill_helper() pair pidfd.c already uses to restore dead standalone pidfds, now exported through pidfd.h. Known fidelity limit: if the receiver also has SO_PASSCRED, the SCM_CREDENTIALS view of such a packet shows the helper's pid instead of the original dead pid. Packets of dead senders on sockets with only SO_PASSCRED are still dropped as before. Signed-off-by: Ahmed Elaidy --- criu/include/pidfd.h | 2 ++ criu/include/sk-queue.h | 1 + criu/pidfd.c | 4 +-- criu/sk-queue.c | 69 +++++++++++++++++++++++++++++++++-------- images/sk-packet.proto | 2 ++ 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/criu/include/pidfd.h b/criu/include/pidfd.h index bcc0fb45a..2ddce7786 100644 --- a/criu/include/pidfd.h +++ b/criu/include/pidfd.h @@ -8,6 +8,8 @@ extern const struct fdtype_ops pidfd_dump_ops; extern struct collect_image_info pidfd_cinfo; extern int is_pidfd_link(char *link); extern void init_dead_pidfd_hash(void); +extern int create_tmp_process(void); +extern int kill_helper(pid_t pid); struct pidfd_dump_info { PidfdEntry pidfe; pid_t pid; diff --git a/criu/include/sk-queue.h b/criu/include/sk-queue.h index 6723c6c7a..af5527d88 100644 --- a/criu/include/sk-queue.h +++ b/criu/include/sk-queue.h @@ -4,6 +4,7 @@ extern struct collect_image_info sk_queues_cinfo; #define SK_QUEUE_REAL_PID 0x1 /* scm creds contains a real pid */ +#define SK_QUEUE_ALLOW_STALE_PID 0x2 /* receiver mints pidfds from skb pids, keep packets of dead senders */ extern int dump_sk_queue(int sock_fd, int sock_id, int flags); extern int sk_queue_post_actions(void); extern int restore_sk_queue(int fd, unsigned int peer_id); diff --git a/criu/pidfd.c b/criu/pidfd.c index ae32025b0..bf7449f60 100644 --- a/criu/pidfd.c +++ b/criu/pidfd.c @@ -121,7 +121,7 @@ static int pidfd_open(pid_t pid, int flags) return syscall(__NR_pidfd_open, pid, flags); } -static int create_tmp_process(void) +int create_tmp_process(void) { int tmp_process; tmp_process = fork(); @@ -135,7 +135,7 @@ static int create_tmp_process(void) return tmp_process; } -static int kill_helper(pid_t pid) +int kill_helper(pid_t pid) { int status; sigset_t blockmask, oldmask; diff --git a/criu/sk-queue.c b/criu/sk-queue.c index 37c023429..2fce4f596 100644 --- a/criu/sk-queue.c +++ b/criu/sk-queue.c @@ -24,6 +24,7 @@ #include "util.h" #include "sk-queue.h" +#include "pidfd.h" #include "files.h" #include "protobuf.h" #include "images/sk-packet.pb-c.h" @@ -40,6 +41,7 @@ struct sk_packet { }; unsigned scm_len; int *scm; + bool allow_stale_pid; }; static LIST_HEAD(packets_list); @@ -186,15 +188,28 @@ int sk_queue_post_actions(void) } } - if (!found) { + if (found) { + pr_debug("ucred: Fixup ucred pids %d -> %d\n", + ue->pid, vpid(item)); + ue->pid = vpid(item); + } else if (pkt->allow_stale_pid) { + /* + * The sender is gone, but the receiver + * has SO_PASSPIDFD: a pidfd minted from + * the skb pid is stale both before and + * after C/R, so the exact pid doesn't + * matter. Mark the packet, restore will + * attach a short-lived helper pid. + */ + pr_debug("ucred: Sender pid %d is dead, marking packet stale on id_for %x\n", + ue->pid, pkt->entry->id_for); + ue->has_stale = true; + ue->stale = true; + } else { pr_warn("ucred: Can't find process with pid %d, ignoring packet\n", ue->pid); goto next; } - - pr_debug("ucred: Fixup ucred pids %d -> %d\n", - ue->pid, vpid(item)); - ue->pid = vpid(item); } ret = pb_write_one(img, pkt->entry, PB_SK_QUEUES); @@ -219,7 +234,7 @@ next: return ret; } -static int queue_packet_entry(SkPacketEntry *entry, void *data, size_t len) +static int queue_packet_entry(SkPacketEntry *entry, void *data, size_t len, int flags) { SkPacketEntry *pe; SkUcredEntry *ue; @@ -246,6 +261,7 @@ static int queue_packet_entry(SkPacketEntry *entry, void *data, size_t len) pkt->entry = pe; pkt->data_off = p - (void *)pkt; + pkt->allow_stale_pid = !!(flags & SK_QUEUE_ALLOW_STALE_PID); pe->id_for = entry->id_for; pe->length = entry->length; @@ -470,7 +486,7 @@ static int dump_sk_queue_packet(int sock_fd, int sock_id, void *data, int size, if (ret > 0) { if (ret == 1) { - if (queue_packet_entry(&pe, data, pe.length)) + if (queue_packet_entry(&pe, data, pe.length, flags)) goto cleanup_packet; } exit_code = 0; @@ -558,6 +574,13 @@ int dump_sk_queue(int sock_fd, int sock_id, int flags) goto err_set_sock; } + /* + * A pidfd minted from the pid of a dead sender is stale + * both before and after C/R, so such packets can be kept. + */ + if (passpidfd) + flags |= SK_QUEUE_ALLOW_STALE_PID; + tmp = sizeof(passcred); if (getsockopt(sock_fd, SOL_SOCKET, SO_PASSCRED, &passcred, &tmp)) { pr_perror("Unable to get SO_PASSCRED"); @@ -614,6 +637,7 @@ static int send_one_pkt(int fd, struct sk_packet *pkt) struct iovec iov; char cmsg[CMSG_MAX_SIZE]; struct cmsghdr *ch = NULL; + pid_t helper_pid = 0; mh.msg_iov = &iov; mh.msg_iovlen = 1; @@ -660,7 +684,7 @@ static int send_one_pkt(int fd, struct sk_packet *pkt) * boundaries messages should be saved. */ - if (entry->ucred && entry->ucred->pid) { + if (entry->ucred && (entry->ucred->pid || entry->ucred->stale)) { struct ucred *ucred; ch = ch ? CMSG_NXTHDR(&mh, ch) : CMSG_FIRSTHDR(&mh); @@ -674,15 +698,30 @@ static int send_one_pkt(int fd, struct sk_packet *pkt) CMSG_SPACE(sizeof(struct ucred)) >= sizeof(cmsg)); ucred = (struct ucred *)CMSG_DATA(ch); - ucred->pid = entry->ucred->pid; + if (entry->ucred->stale) { + /* + * The sender died before the dump. Attach the pid of + * a short-lived helper process to the skb; the helper + * is killed right after sendmsg(), so a receiver with + * SO_PASSPIDFD gets a stale pidfd, just as before the + * dump. + */ + helper_pid = create_tmp_process(); + if (helper_pid < 0) + return -1; + ucred->pid = helper_pid; + } else { + ucred->pid = entry->ucred->pid; + } ucred->uid = entry->ucred->uid; ucred->gid = entry->ucred->gid; msg_controllen += CMSG_SPACE(sizeof(struct ucred)); - pr_debug("\tsend creds pid %d uid %d gid %d\n", - entry->ucred->pid, - entry->ucred->uid, - entry->ucred->gid); + pr_debug("\tsend creds pid %d uid %d gid %d%s\n", + ucred->pid, + ucred->uid, + ucred->gid, + entry->ucred->stale ? " (stale pid helper)" : ""); } mh.msg_controllen = msg_controllen; @@ -690,6 +729,10 @@ static int send_one_pkt(int fd, struct sk_packet *pkt) ret = sendmsg(fd, &mh, 0); xfree(pkt->data); xfree(pkt->scm); + + if (helper_pid > 0 && kill_helper(helper_pid)) + return -1; + if (ret < 0) { pr_perror("Failed to send packet"); return -1; diff --git a/images/sk-packet.proto b/images/sk-packet.proto index 341892607..202859e75 100644 --- a/images/sk-packet.proto +++ b/images/sk-packet.proto @@ -11,6 +11,8 @@ message sk_ucred_entry { required uint32 uid = 1; required uint32 gid = 2; required uint32 pid = 3; + // The sender died before the dump, pid can't be restored + optional bool stale = 4; } message sk_packet_entry { From b782ad993b082943336e11b611022e9fa76400a5 Mon Sep 17 00:00:00 2001 From: Ahmed Elaidy Date: Thu, 16 Jul 2026 18:46:07 +0300 Subject: [PATCH 9/9] zdtm: add sk-unix-dgram-pidfd-stale test for stale pidfd in queue A child queues a message to a SO_PASSPIDFD socket, exits and is reaped before the dump, so the skb pid references a dead process. After checkpoint/restore the test verifies the packet survived and that recvmsg yields a pidfd that behaves exactly as it would have before the dump. Only kernels >= 6.17 can mint a pidfd for an already reaped process (see kernel commit "af_unix: enable handing out pidfds for reaped tasks in SCM_PIDFD"); older kernels put a negative error code into the cmsg payload instead. The test probes the kernel's behavior with a throwaway socketpair before the dump and asserts the post-restore behavior matches: on new kernels the restored pidfd must be stale (pidfd_send_signal() fails with ESRCH, fdinfo shows Pid: -1), on old kernels the receiver must get a negative value, as it would have pre-dump. Signed-off-by: Ahmed Elaidy --- test/zdtm/static/Makefile | 1 + test/zdtm/static/sk-unix-dgram-pidfd-stale.c | 236 ++++++++++++++++++ .../static/sk-unix-dgram-pidfd-stale.desc | 1 + 3 files changed, 238 insertions(+) create mode 100644 test/zdtm/static/sk-unix-dgram-pidfd-stale.c create mode 100644 test/zdtm/static/sk-unix-dgram-pidfd-stale.desc diff --git a/test/zdtm/static/Makefile b/test/zdtm/static/Makefile index 82f47d870..6169b55f1 100644 --- a/test/zdtm/static/Makefile +++ b/test/zdtm/static/Makefile @@ -490,6 +490,7 @@ TST_DIR = \ sk-unix-dgram-cred \ sk-unix-dgram-pidfd \ sk-unix-dgram-pidfd-cred \ + sk-unix-dgram-pidfd-stale \ sk-unix-dgram-ghost \ unsupported_children_collision \ shared_slave_mount_children \ diff --git a/test/zdtm/static/sk-unix-dgram-pidfd-stale.c b/test/zdtm/static/sk-unix-dgram-pidfd-stale.c new file mode 100644 index 000000000..54aac4d21 --- /dev/null +++ b/test/zdtm/static/sk-unix-dgram-pidfd-stale.c @@ -0,0 +1,236 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "zdtmtst.h" + +#ifndef SO_PASSPIDFD +#define SO_PASSPIDFD 76 +#endif + +#ifndef SCM_PIDFD +#define SCM_PIDFD 0x04 +#endif + +const char *test_doc = "Test C/R of a stale pidfd in unix socket queue (sender died before dump)\n"; +const char *test_author = "Ahmed Elaidy "; + +char *dirname; +TEST_OPTION(dirname, string, "directory name", 1); + +static int pidfd_send_signal(int pidfd, int sig, siginfo_t *info, unsigned int flags) +{ + return syscall(__NR_pidfd_send_signal, pidfd, sig, info, flags); +} + +static pid_t pidfd_get_pid(int pidfd) +{ + char path[64]; + char line[256]; + pid_t pid = -2; + FILE *f; + + snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", pidfd); + f = fopen(path, "r"); + if (!f) { + pr_perror("fopen %s", path); + return -2; + } + + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "Pid: %d", &pid) == 1) + break; + } + + fclose(f); + return pid; +} + +/* + * Queue one message into sk_snd from a child which exits and is + * reaped before we return, so the skb pid points to a dead process. + */ +static int queue_msg_from_dead_child(int sk_snd) +{ + int status; + pid_t child; + + child = fork(); + if (child < 0) + return pr_perror("fork"); + + if (child == 0) { + char buf[] = "hello"; + struct iovec iov = { + .iov_base = buf, + .iov_len = sizeof(buf), + }; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + }; + + if (sendmsg(sk_snd, &msg, 0) < 0) + _exit(1); + _exit(0); + } + + if (waitpid(child, &status, 0) != child) + return pr_perror("waitpid"); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + pr_err("child failed to send\n"); + return -1; + } + + return 0; +} + +static int recv_pidfd(int sk_rcv, int *pidfd) +{ + struct msghdr msg = {}; + struct iovec iov; + char buf[64]; + char cmsg_buf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr *cmsg; + + memset(buf, 0, sizeof(buf)); + memset(cmsg_buf, 0, sizeof(cmsg_buf)); + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf; + msg.msg_controllen = sizeof(cmsg_buf); + + if (recvmsg(sk_rcv, &msg, 0) < 0) + return pr_perror("recvmsg"); + + cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg) { + pr_err("no cmsg\n"); + return -1; + } + + if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_PIDFD) { + pr_err("wrong cmsg: level %d type %d\n", + cmsg->cmsg_level, cmsg->cmsg_type); + return -1; + } + + memcpy(pidfd, CMSG_DATA(cmsg), sizeof(*pidfd)); + return 0; +} + +/* + * Only kernels >= 6.17 can mint a pidfd for an already reaped + * process; older ones put a negative error code into the cmsg + * payload instead. Probe which behavior this kernel has, so we can + * check that C/R preserves it. + */ +static int probe_reaped_pidfd(void) +{ + int sk[2]; + int opt = 1; + int pidfd; + + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sk) < 0) { + pr_perror("socketpair"); + return -1; + } + + if (setsockopt(sk[1], SOL_SOCKET, SO_PASSPIDFD, &opt, sizeof(opt)) < 0) { + pr_perror("setsockopt SO_PASSPIDFD"); + return -1; + } + + if (queue_msg_from_dead_child(sk[0])) + return -1; + + if (recv_pidfd(sk[1], &pidfd)) + return -1; + + close(sk[0]); + close(sk[1]); + + if (pidfd < 0) + return 0; + + close(pidfd); + return 1; +} + +int main(int argc, char *argv[]) +{ + int sk[2]; + int opt = 1; + int pidfd; + int reaped_pidfd_supported; + + test_init(argc, argv); + + reaped_pidfd_supported = probe_reaped_pidfd(); + if (reaped_pidfd_supported < 0) + return 1; + + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sk) < 0) + return pr_perror("socketpair"); + + if (setsockopt(sk[1], SOL_SOCKET, SO_PASSPIDFD, &opt, sizeof(opt)) < 0) + return pr_perror("setsockopt SO_PASSPIDFD"); + + if (queue_msg_from_dead_child(sk[0])) + return 1; + + test_daemon(); + test_waitsig(); + + /* The packet must survive C/R and yield a stale pidfd */ + if (recv_pidfd(sk[1], &pidfd)) { + fail("no SCM_PIDFD cmsg after restore"); + return 1; + } + + if (!reaped_pidfd_supported) { + /* + * Pre-dump recvmsg would have failed to mint a pidfd + * for the dead sender the same way. + */ + if (pidfd >= 0) { + close(pidfd); + fail("got pidfd %d, but kernel can't mint pidfds of reaped processes", pidfd); + return 1; + } + test_msg("kernel can't mint pidfds of reaped processes, got %d as before dump\n", pidfd); + } else { + if (pidfd < 0) { + fail("invalid pidfd %d after restore", pidfd); + return 1; + } + + /* The pidfd must reference a dead process, as before the dump */ + if (pidfd_send_signal(pidfd, 0, NULL, 0) == 0 || errno != ESRCH) { + fail("pidfd is not stale after restore"); + return 1; + } + + if (pidfd_get_pid(pidfd) != -1) { + fail("pidfd fdinfo Pid is not -1 after restore"); + return 1; + } + + close(pidfd); + } + + close(sk[0]); + close(sk[1]); + + pass(); + return 0; +} diff --git a/test/zdtm/static/sk-unix-dgram-pidfd-stale.desc b/test/zdtm/static/sk-unix-dgram-pidfd-stale.desc new file mode 100644 index 000000000..8e9526ec4 --- /dev/null +++ b/test/zdtm/static/sk-unix-dgram-pidfd-stale.desc @@ -0,0 +1 @@ +{'flavor': 'h ns uns', 'feature': 'so_passpidfd'}