sk-queue: add SCM_PIDFD / SO_PASSPIDFD checkpoint/restore support - #3093
sk-queue: add SCM_PIDFD / SO_PASSPIDFD checkpoint/restore support#30933idey wants to merge 15 commits into
Conversation
6892d4e to
e0860de
Compare
|
The Force-pushed: the test now probes the kernel's behavior with a throwaway socketpair before the dump and asserts post-restore behavior matches full stale checks (ESRCH, fdinfo Two known limits of the stale-packet handling worth noting, besides the one in the commit message:
Happy to do these as follow-ups or fold them into this series, whichever you prefer. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## criu-dev #3093 +/- ##
============================================
- Coverage 57.72% 57.48% -0.24%
============================================
Files 161 161
Lines 43920 44265 +345
Branches 9637 9712 +75
============================================
+ Hits 25351 25446 +95
- Misses 18330 18579 +249
- Partials 239 240 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
What about this error: I have not seen that one before. |
Yes, that one is on me, since kernel 6.16 (commit 7d8d93fdde50) SO_PASSPIDFD is restricted to AF_UNIX and returns EOPNOTSUPP on netlink sockets, but I was probing it in the shared unix/netlink branch of dump_socket_opts(). That's why this job was failing. I will make it for AF_UNIX only. |
b782ad9 to
c4172c8
Compare
|
@mihalicyn, i think you force pushed by accident:) |
448b489 to
e330c15
Compare
no, it's just because |
cf27a31 to
80a6ee6
Compare
c5aaeb8 to
b50f4ea
Compare
| continue; | ||
|
|
||
| if (entry->dead_pid) { | ||
| dead_pid = dead_pid_get(&dead_pids, entry->dead_pid); |
There was a problem hiding this comment.
Idea of this code is clear, but not fully correct. You can't lookup dead_pid just from attributes, because you can have two different processes with the same exit code, for instance. And what happens here is that user at the end will get the same pidfd at the end.
A correct way to identify struct pid these days is to rely on inode number (ino), see for instance https://github.com/torvalds/linux/blob/cee9395acd8043be0644b25c34bfa86623f2b935/fs/pidfs.c#L897.
In fact, we already do this in lookup_dead_pidfd function. You can try to do the same and hold a global hashtable using ino as an index, for example.
But please, before changing your code and reworking this, try to come up with an extra zdtm test reproducing the issue I'm describing here theoretically ;-)
There was a problem hiding this comment.
I guess I'm on my way to figure it out:)
There was a problem hiding this comment.
so what changed is: stand-in processes for dead pids are now keyed on the pidfs inode and kept in one table shared by the whole restore, instead of a table private to each task. So if two tasks hold packets from the same dead sender, they both get the same stand-in and the receivers see one inode, not two. The root task forks them after the tree is up and reaps them once everyone has restored their files.
A task restoring outside the root pid namespace still gets private stand-ins, since a pid from another namespace is not useful to it. Same behaviour as before for that case.
while working I fixed this: a pidfd with no inode recorded no longer shares a stand-in with other such entries, and we now reject an exit status a stand-in cannot actually reproduce instead of silently restoring it as exit(1).
New tests: sk-unix-dgram-pidfd-ino (two senders that died the same way stay distinct) and sk-unix-dgram-pidfd-xtask (one dead sender across two tasks stays one inode).
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 <elaidya225@gmail.com>
These constants were introduced in kernel 6.5 ("scm: add SO_PASSPIDFD
and SCM_PIDFD") 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 <elaidya225@gmail.com>
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 <elaidya225@gmail.com>
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 <elaidya225@gmail.com>
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. The toggle is undone on every exit path of dump_sk_queue(), the error ones included, but not if criu is killed in between: the socket would then be left with SO_PASSCRED its owner never set and would start seeing SCM_CREDENTIALS cmsgs it does not expect. SO_PEEK_OFF, toggled a few lines above, has exactly the same exposure. 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 <elaidya225@gmail.com>
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 <elaidya225@gmail.com>
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 <elaidya225@gmail.com>
The pidfs PIDFD_GET_INFO ioctl (Linux 6.13) reports what the kernel keeps about the process a pidfd refers to, including the wait(2)-style exit code pidfs stashes on its struct pid when it exits. That survives reaping, so it is the only way to learn how a process we can no longer wait(2) for died. Add pidfd_query_exit(), a thin wrapper over it. The struct layout is versioned by size and baked into the ioctl number, and the kernel serves any request at least as large as the first published version, so we pin our own copy to that 64-byte version -- exit_code, the only member we need, is its last field -- rather than track the ones added since. This needs no <linux/pidfd.h> and works unchanged on every kernel that has the ioctl. Probe support by issuing it on a pidfd of ourselves: a live process reports "no exit info" (the ioctl works), while an unsupported kernel fails it. Expose the result as kdat.has_pidfd_get_info and a "pidfd_get_info" cr-check feature, so its users and the zdtm tests can tell whether definitive exit status information is available at all before asking for it. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
88b6de2 to
23cd2ff
Compare
CRIU already restores a pidfd whose task was reaped before the dump: the task is gone, so open_one_pidfd() forks a throwaway process, opens a pidfd of it and kills it, leaving the restored pidfd as stale as the original one. What it does not restore is *how* the process died. The kernel keeps that around. When a task exits, pidfs_exit() stashes its wait(2)-style exit code in the pidfs attributes hanging off its struct pid (struct pidfs_attr in fs/pidfs.c), which is what PIDFD_GET_INFO reports for PIDFD_INFO_EXIT long after the task has been reaped. A restored pidfd currently reports SIGKILL there -- the way the throwaway process was disposed of -- no matter how the original died. So dump that pidfs state alongside the pidfd and reproduce it: - dump_one_pidfd() asks pidfd_query_exit() for the exit status of a pidfd whose task is already reaped, on kernels that have the ioctl. - The image gains pidfs_attr_entry, mirroring struct pidfs_attr. It lives in its own images/pidfs.proto rather than inside pidfd.proto because a struct pid is shared by everything that refers to the process, so its pidfs state is described once and referenced from each such image. Only the exit code is modelled: cgroupid is meaningless after migration, and the coredump attributes would require the stand-in to actually dump core. - dead_pid_get()/dead_pid_put_all() replace the open-coded fork-and- kill in open_one_pidfd() with a small pool of stand-in processes. A stand-in blocks on a pipe until told which wait(2) status to reproduce -- exit with a code, or unblock and raise a signal -- and is disposed of only once everything that needs to reference the dead pid has done so. Stand-ins are shared by exit status, as nothing observable distinguishes two dead pids that died the same way. With no recorded status (an older kernel, or a pidfd we could not query) the previous plain SIGKILL is used. The SIGCHLD-blocking preamble shared with kill_helper() is factored out into a small helper, and the error paths in open_one_pidfd() no longer leak the stand-in process. kill_helper() now unblocks SIGCHLD on the way out however it got there; it used to return with the signal still blocked when the kill or the wait failed, which would keep sigchld_handler() from ever running again in a task that has a whole restore still ahead of it. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
pidfd_dead already leaves two pidfds of processes that died differently -- the child exits with 0, the grandchild is killed with SIGKILL -- so it is the natural place to check that PIDFD_GET_INFO keeps reporting how each of them died after C/R. The check is skipped on kernels without the ioctl (< 6.13), where the exit status cannot be read before the dump either. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
A receiver with SO_PASSPIDFD mints a pidfd from the struct pid the kernel attached to each queued skb. When the sender has already exited and been reaped, that pidfd is stale, and it must stay stale across C/R. On dump we already receive an authoritative pidfd for the peeked packet (recvmsg installs it because SO_PASSCRED is toggled on for the peek), so inspect it instead of closing it blindly. The cmsg payload is pidfd_prepare()'s raw return value: -EINVAL (before v6.10) or -ESRCH means the kernel refused to mint a pidfd for an already reaped task, and PIDFD_GET_INFO reporting an exit means the same on kernels that do mint one. Both prove the sender is dead, and its wait(2) exit status is recorded in the image when known. The remaining errors pidfd_prepare() can fail with describe the dumping process rather than the sender -- an exhausted fd table, no memory -- so they abort the dump instead of being read as a death certificate for a sender that is very much alive. A sender proven alive yet living outside the dumped tree cannot be restored faithfully either, so its packet is dropped rather than mislabelled stale. Kernels without PIDFD_GET_INFO fall back to the previous pstree-lookup-miss heuristic. The dead sender's pid number is a dump-host real pid with no meaning after migration, so it is zeroed. A packet whose SCM_CREDENTIALS pid is zero and for which no pidfd verdict came back keeps being written as is. A zero pid does not mean the skb carries no struct pid: scm_set_cred() keeps the struct pid and derives the number from it with pid_vnr(), which yields 0 for a pid that cannot be named in the pid namespace we peeked the queue from. The pidfd is minted from that same struct pid, so a verdict does apply to such packets, and one that proves the sender dead is handled as stale above in preference to writing the packet bare. The peek now also fails the dump on MSG_CTRUNC. The control buffer is 2048 bytes against a worst case of about 1088, so this should not be reachable; but truncation would drop the SCM_PIDFD cmsg and make a dead sender look alive, and today it silently loses passed SCM_RIGHTS fds. On restore, stale packets are re-sent with spoofed SCM_CREDENTIALS carrying the pid of a stand-in process from the pidfd layer's dead pid pool, so the receiver mints a pidfd that goes stale as soon as the stand-in is reaped -- which happens only after the queue is refilled, so every skb already holds a reference on the stand-in's struct pid. The pool reproduces the recorded exit status, so PIDFD_GET_INFO on the restored pidfd reports the way the original sender died; a packet whose status the kernel could not tell us gets a plain stand-in instead. Stand-ins are shared by exit status, so two senders that died the same way end up behind a single one. That collapses their identities: before the dump a receiver could tell the two apart by the pid in SCM_CREDENTIALS or by the inode of the pidfds it minted from them, and afterwards it cannot. Keeping them distinct would need a per-sender key in the image -- the pidfs inode of the peeked pidfd, which is what pidfd_entry already records -- and one stand-in per struct pid, shared with the pidfd file layer. That is left for a follow-up. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
Queue two datagrams into an SO_PASSPIDFD socket from children that are reaped before the dump -- one exiting with a code, one killed by a signal -- so their skb pids are stale and, on kernels that record exit info, carry distinct statuses (exercising restore's per-status helper grouping). Kernel behaviour is probed before the dump on a throwaway socketpair: only kernels >= 6.17 mint a pidfd for a reaped sender, and PIDFD_GET_INFO / PIDFD_INFO_EXIT may or may not report its exit status. After C/R the test asserts the restored pidfds match that pre-dump behaviour: a negative cmsg payload on old kernels, or a valid-but-stale pidfd (ESRCH, fdinfo Pid: -1) whose PIDFD_GET_INFO reports the original exit code / terminating signal on new ones. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
A process that was already dead at dump time is restored as a stand-in process that everything takes its references from, and that is then made to die the way the original did. Stand-ins were shared by exit status, which is not an identity: two processes that died the same way collapsed onto one, so a receiver that could tell them apart before the dump could not after it. Key them on the pidfs inode number instead. Since Linux 6.9 pidfs gives every struct pid a unique inode, and comparing inode numbers is how the kernel itself tells two pidfds apart; it is the only handle left on a process that has been reaped. Both sides of the dump read it off a pidfd the same way, with pidfd_query_ino(): fstat() for i_ino, plus FS_IOC_GETVERSION for the high half of the pidfs cookie that pidfs_init_inode() puts in i_generation on a 32-bit kernel. dump_one_pidfd() prefers that to the ino: line of /proc/pid/fdinfo/N it used to take, which carries i_ino alone, and dump_packet_cmsg() reads it off the pidfd it peeks before closing it, recording it in pidfs_attr_entry. pidfd_entry.ino grows from uint32 to uint64 to hold it, which is wire compatible: both are varints. One dead process has to come back as one struct pid however many references there are, and those references are spread across tasks: two sockets in two tasks can each hold a packet it sent, and a third task can hold a pidfd of it. So the stand-ins move from a function-local pool to one table shared by the whole restore. It is built while the images are collected, with dead_pid_add() handing out an index per distinct struct pid, and moved into shared memory before the task tree is forked. The root task forks one stand-in per entry, once the whole tree exists -- forking earlier would take pid numbers the restored tasks need -- and before any task starts restoring its files, so the pids are there by the time anything looks them up. It kills and reaps them once every task has restored all of its files, which is where the last reference to a dead pid was taken. A status helper therefore outlives the open method that forked it, so it no longer holds the write end of a pipe -- it would sit in the way of a file still to be restored -- and waits for a signal instead. Stand-ins also close the fds they inherit and take a PR_SET_PDEATHSIG for the same reasons. A task restoring in a pid namespace of its own is the exception: a pid number of the root task's namespace means nothing there, so it forks private stand-ins and disposes of them from open_fdinfos(). Two tasks in different pid namespaces referring to one dead process still get a stand-in each. An entry with no inode recorded gets a stand-in of its own, shared with nothing, rather than one shared by guesswork. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
A struct pid is identified by its pidfs inode number, not by how the process died: since Linux 6.9 two pidfds compare equal exactly when their inode numbers do, and that is the only handle userspace has on the identity of a process that has already been reaped. Queue three datagrams into an SO_PASSPIDFD socket from children that all exit 42 and are reaped before the dump, so nothing but the struct pid tells their senders apart, and hold a pidfd of the first one. After C/R the test asserts what the kernel guaranteed before it: the held pidfd and the packet of that same sender still share a pidfs ino, and the three senders still have three distinct ones. The first sender's packet sits at the head of the queue on purpose, so the held-vs-queued equality is also asserted with MSG_PEEK before the dump. A throwaway socketpair probes the kernel first: only kernels >= 6.17 mint a pidfd for a reaped sender, without which there is no identity to observe and the test skips, and the same probe checks the kernel itself holds both relations, so a later failure is unambiguously ours. Both relations are exactly what keying the stand-ins on the pidfs ino buys. Sharing them by exit status out of a pool local to each caller, as the previous commit replaced, gave a pidfd file and a queued packet of one dead pid two stand-ins, and two senders that died the same way one. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
Two tasks each hold a queued packet from the same dead sender, and check their receivers see one pidfs inode, not one per task. Signed-off-by: Ahmed Elaidy <elaidya225@gmail.com>
|
@mihalicyn, The CI is keep failing but I don't think this is my pr doing though:) what i pushed yesterday was stuff i missed but appeared when i took another look:
Plus a fallback for when there is no ino at all, which is every dead sender on a kernel that mints no pidfd for a reaped one. I save the sender's dump host pid and use it only to match ino-less entries against each other. It is not a real identity since pid numbers get reused, but without it every packet of one dead sender would fork its own stand-in. An ino-less entry is never matched against one that has an ino. If two references claim different exit codes for the same dead pid it warns and keeps the first, since the second guess is no better. The rest is comments and a cleanup of the pid branches in sk_queue_post_actions(), no behaviour change there. |
This series adds checkpoint/restore support for the SCM_PIDFD control message and the SO_PASSPIDFD socket option (kernel ≥ 6.5), building on the SCM_CREDENTIALS support merged in #3026.
Approach
SCM_CREDENTIALS and SCM_PIDFD are two
recvmsg-time views of the same per-skb state (a refcountedstruct pid+ creds), so no new packet image format is needed:SO_PASSPIDFDbut notSO_PASSCRED, we temporarily enableSO_PASSCREDaround theMSG_PEEKloop (same pattern as theSO_PEEK_OFFtoggling), so queued pids are dumped through the existingsk_ucred_entrypath. TheSCM_PIDFDcmsg only carries an fd minted for us atrecvmsgtime, we inspect it for the sender's liveness/exit status (see below) and close it.SO_PASSPIDFDis saved as a new sk-opts field.SCM_CREDENTIALS, which attaches the rightstruct pidto the skb, the restored receiver then mints a correct pidfd onrecvmsgfor free.Gated by a kerndat probe and a new
so_passpidfdcr-check feature.Stale pidfds
A pidfd of a reaped process reports fdinfo
Pid: -1andESRCHonpidfd_send_signal(), so the original pid number is not observable only "a pidfd of a dead process (with a given exit status)" must survive C/R.SCM_PIDFDpayload (kernels < 6.17 refuse to mint a pidfd for a reaped sender) or aPIDFD_GET_INFO/PIDFD_INFO_EXIT-reported exit means the sender is definitively dead, and we record its exit status. A pidfd that proves the sender is alive but outside the dumped tree is dropped rather than mislabeled stale. Kernels without the ioctl fall back to the old pstree-lookup-miss heuristic.exit_code, not just a stale bool. On restore we group one short-lived helper per distinct exit status and have it exit that way, so the restored pidfd reports the original status viaPIDFD_GET_INFOnot the helper'sSIGKILL. Helpers are killed and reaped only after the queue is refilled; each re-sent skb already holds a reference on the helper'sstruct pid.PIDFD_GET_INFOsupport is detected via kerndat and exposed as apidfd_get_infocr-check feature.Known limit (documented in the commit messages): on sockets with both options set, a stale packet's
SCM_CREDENTIALSview shows the helper's pid; stale packets onSO_PASSCRED-only sockets are still dropped as before.Tests
sk-unix-dgram-pidfd— basic SCM_PIDFD C/Rsk-unix-dgram-pidfd-cred—SO_PASSCRED+SO_PASSPIDFDtogether, both cmsgs in onerecvmsgwith matching pidssk-unix-dgram-pidfd-stale— sender reaped before dump; restored pidfd behaves stale (ESRCH, fdinfoPid: -1) and reports the original exit status on kernels withPIDFD_GET_INFO