Skip to content

restore: run CRIU in a child process joined to the container cgroup - #2250

Open
giuseppe wants to merge 2 commits into
containers:mainfrom
giuseppe:criu-restore-in-child
Open

giuseppe wants to merge 2 commits into
containers:mainfrom
giuseppe:criu-restore-in-child

Conversation

@giuseppe

@giuseppe giuseppe commented Sep 11, 2026

Copy link
Copy Markdown
Member

Move the call into a throw-away child process instead: it joins the container cgroup, asks CRIU to restore, and exits. Nothing has to be undone afterwards and does not affect the current process.

A followup to #2241.

@kolyshkin

Copy link
Copy Markdown
Collaborator

Being tested in #2252

@kolyshkin kolyshkin closed this Sep 11, 2026
@kolyshkin kolyshkin reopened this Sep 11, 2026
@kolyshkin

Copy link
Copy Markdown
Collaborator

My bad; clicked the wrong button 😃

The test in #2252 passed, so marking this as ready for review.

@kolyshkin
kolyshkin marked this pull request as ready for review September 11, 2026 20:14
@kolyshkin

Copy link
Copy Markdown
Collaborator

With this PR, a restore without --detach kills the restored container right
after the restore, if its init has a parent death signal set. It is a very niche
case (I mean, who restores a container without -d?) and I think we should
ignore it, but nevertheless all the details are below.

Gory Details CRIU checkpoints and restores `pdeath_sig` (`criu/cr-dump.c` and `criu/pie/restorer.c:restore_pdeath_sig`). With `criu_restore_child()`, which sets `rst_sibling`, CRIU creates the root task with `CLONE_PARENT`, so its parent is whoever called into libcriu. The comment in `criu/cr-restore.c` (`maybe_clone_parent`) says this is deliberate: "This is the only way to correctly restore the pdeath_sig of the root task".

Before this PR, the caller was crun itself, which stays alive for as long as
the container runs, so the signal was not delivered. Now the caller is the
throw-away process created by criu_restore_child_in_cgroup(), which exits as
soon as the restore is over. When it does, the kernel reparents the restored
init and, in the same loop, sends it its parent death signal
(forget_original_parent() in kernel/exit.c):

	list_for_each_entry(p, &father->children, sibling) {
		for_each_thread(p, t) {
			RCU_INIT_POINTER(t->real_parent, reaper);
			...
			if (t->pdeath_signal)
				group_send_sig_info(t->pdeath_signal,
						    SEND_SIG_NOINFO, t,
						    PIDTYPE_TGID);

Being a subreaper does not help: the signal is sent no matter who the new
parent is. The container having its own PID namespace does not help either:
the signal comes from a process in an ancestor PID namespace, so
send_signal_locked() sets force and the init does not ignore it.

--detach is not affected, as crun exits there anyway.

I tested this with an addition to the test added by this PR: a test init that
sets PR_SET_PDEATHSIG to SIGKILL once it receives SIGUSR1 (so that the
signal is set after the runtime which started the container is gone), then a
checkpoint and a foreground restore of that container. On this PR, the restored
container is gone right after the restore:

not ok 2 - checkpoint-restore-foreground-pdeathsig
# test_cr_restore_foreground_pdeathsig: the container was not restored:

crun restore prints nothing to stderr: the restore itself succeeds, and the
init is killed afterwards. Without the first two commits of this PR, the same
test passes. The test (on top of this PR) is in kolyshkin@c769ba51.

As far as I can see, a fix has to keep the process which calls
criu_restore_child() alive for the lifetime of the container, i.e. it has to
be crun itself (like it was before this PR).

Comment thread src/libcrun/criu.c Outdated
Comment on lines +1355 to +1356
ret = criu_restore_child_in_cgroup (status->cgroup_path, &criu_ret, err);
if (UNLIKELY (ret < 0))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So once the restore is done we should stop being a subreaper (as we only wait for init). I would add this:

       ret = criu_restore_child_in_cgroup (status->cgroup_path, &criu_ret, err);
+      if (! cr_options->detach && UNLIKELY (prctl (PR_SET_CHILD_SUBREAPER, 0, 0, 0, 0) < 0))
+        libcrun_warning ("cannot stop being a child subreaper: %s", strerror (errno));
       if (UNLIKELY (ret < 0))

I have a test case for this but it's way too big and probably won't worth adding here. You can see it kolyshkin@ed0a165.

My thinking is it's a niche case but the fix is easy so why not add it.

@kolyshkin kolyshkin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two issues with a foreground restore; the first can probably be ignored, the second is an easy fix so I'd add it. PTAL @giuseppe

@giuseppe

Copy link
Copy Markdown
Member Author

With this PR, a restore without --detach kills the restored container right after the restore, if its init has a parent death signal set. It is a very niche case (I mean, who restores a container without -d?) and I think we should ignore it, but nevertheless all the details are below.

thanks for trying that. This is indeed an issue, and if we don't want to just error out, it is better to leave the code as it is.

For users using libcrun (if any...), I've added a new function to introduce the behavior I've suggested earlier

@giuseppe
giuseppe force-pushed the criu-restore-in-child branch from 5869e6c to 07dcbe3 Compare September 13, 2026 16:58

@kolyshkin kolyshkin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably be a crun restore option, too.

Yet better, check (from a crun dump) if the dumped init has a parent death signal set. If it's not, we can use PREFORK, otherwise we can't.

@kolyshkin kolyshkin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing -- when detach == true, we can (and should) use PREFORK, as the issue is only with foreground restore.

This way we'll also have some CI coverage for this code (which is currently absent).

Comment thread src/libcrun/criu.c Outdated
SIGCHLD. Its result is still visible, so use it. */
if (errno != ECHILD)
return crun_make_error (err, errno, "waitpid for the CRIU restore process");
return 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we can lose real err here.

Suggested change
return 0;
/* The child might have failed before calling CRIU, and left an error. */
return *err != NULL ? -1 : 0;

@kolyshkin

Copy link
Copy Markdown
Collaborator

One more thing:

Subject: [PATCH] criu: block signals across the vfork for restore

The throw-away process used with LIBCRUN_RESTORE_OPTIONS_PREFORK shares
the memory and the stack with the caller, and it runs for the whole
restore: libcriu forks CRIU, talks to it over RPC and waits for it.  If
a signal is delivered to it in the meantime, a handler installed by the
program embedding libcrun runs there, and can corrupt the state of the
suspended caller.

Do what posix_spawn does: block all signals before the vfork, reset the
handlers to SIG_DFL in the child (keeping the ignored signals ignored,
as CRIU would inherit them otherwise too), and restore the original
signal mask both in the child, before calling libcriu, and in the
parent, once the child is gone.

Signed-off-by: Kir Kolyshkin <[email protected]>
---
 src/libcrun/criu.c | 42 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 38 insertions(+), 4 deletions(-)

diff --git a/src/libcrun/criu.c b/src/libcrun/criu.c
index 52307f88..2427011a 100644
--- a/src/libcrun/criu.c
+++ b/src/libcrun/criu.c
@@ -25,6 +25,7 @@
 #  include <sys/types.h>
 #  include <criu/criu.h>
 #  include <sched.h>
+#  include <signal.h>
 #  include <sys/stat.h>
 #  include <sys/mount.h>
 #  include <fcntl.h>
@@ -992,26 +993,56 @@ move_back_to_cgroups (const char *cgroups)
    process tree is a sibling of CRIU, hence a child of the process calling
    criu_restore_child().
 
-   The child is kept as small as possible, as it shares the memory with the
-   caller.  This is a separate function so that no variable of the caller is
-   live across the vfork.  */
+   As the child shares the memory and the stack with the caller, all signals
+   are blocked across the vfork, and the child resets the signal handlers
+   before unblocking them, so that no handler of the caller can run there.
+   This is a separate function so that no variable of the caller is live
+   across the vfork.  */
 static int
 criu_restore_child_in_cgroup (const char *cgroup_path, int *criu_ret, libcrun_error_t *err)
 {
+  sigset_t all_signals, old_mask;
   int wait_status = 0;
   pid_t pid;
   int ret;
 
   *criu_ret = -1;
 
+  sigfillset (&all_signals);
+  ret = sigprocmask (SIG_BLOCK, &all_signals, &old_mask);
+  if (UNLIKELY (ret < 0))
+    return crun_make_error (err, errno, "sigprocmask");
+
   /* Must be vfork: the child shares our memory space, so both *criu_ret and
      the error it creates are visible here once it is gone.  */
   pid = vfork ();
   if (UNLIKELY (pid < 0))
-    return crun_make_error (err, errno, "vfork");
+    {
+      int saved_errno = errno;
+
+      sigprocmask (SIG_SETMASK, &old_mask, NULL);
+      return crun_make_error (err, saved_errno, "vfork");
+    }
 
   if (pid == 0)
     {
+      struct sigaction act;
+      int i;
+
+      /* The signal dispositions are not shared with the parent, so resetting
+         them here does not affect it.  Keep the ignored signals ignored, as
+         CRIU would inherit them if it was run directly by the caller.  */
+      for (i = 1; i < NSIG; i++)
+        {
+          if (sigaction (i, NULL, &act) < 0 || act.sa_handler == SIG_IGN || act.sa_handler == SIG_DFL)
+            continue;
+
+          memset (&act, 0, sizeof (act));
+          act.sa_handler = SIG_DFL;
+          sigaction (i, &act, NULL);
+        }
+      sigprocmask (SIG_SETMASK, &old_mask, NULL);
+
       if (! is_empty_string (cgroup_path))
         {
           ret = libcrun_move_process_to_cgroup (0, 0, cgroup_path, false, err);
@@ -1023,6 +1054,9 @@ criu_restore_child_in_cgroup (const char *cgroup_path, int *criu_ret, libcrun_er
       _safe_exit (EXIT_SUCCESS);
     }
 
+  /* The child is gone by now, it is safe to handle signals again.  */
+  sigprocmask (SIG_SETMASK, &old_mask, NULL);
+
   ret = waitpid_ignore_stopped (pid, &wait_status, 0);
   if (UNLIKELY (ret < 0))
     {
-- 
2.55.0

@kolyshkin

Copy link
Copy Markdown
Collaborator

This could probably be a crun restore option, too.

Yet better, check (from a crun dump) if the dumped init has a parent death signal set. If it's not, we can use PREFORK, otherwise we can't.

Answering to myself -- option is probably a bad idea, no one knows how/when to use it.

Checking the dump is probably somewhat complicated (haven't tried), so we can leave it for later.

Using the PREFORK when detach == true now should be implemented I think.

giuseppe and others added 2 commits September 14, 2026 11:22
CRIU restores the container as a child of the process driving it, which
must also join the container cgroup for the time of the restore, and move
back out of it afterwards.

With `--detach` there is no reason for that process to be crun itself: it
does not wait for the container, so being its parent is useless, and the
container is reparented as soon as crun exits.  It is worse for a program
embedding libcrun, which gets moved in and out of the container cgroup and
is left with the container init as a child it has to reap.

Use a throw-away process instead, the way libcrun_container_run already
forks when detaching.

A foreground restore is unchanged: the container must stay a child of crun
for crun to wait for it, and a throw-away process cannot be used there, as
the restored init gets its parent death signal as soon as the process which
drove the restore is gone.

Signed-off-by: Giuseppe Scrivano <[email protected]>
The throw-away process used by a detached restore shares the memory and
the stack with the caller, and it runs for the whole restore: libcriu
forks CRIU, talks to it over RPC and waits for it.  If a signal is
delivered to it in the meantime, a handler installed by the program
embedding libcrun runs there, and can corrupt the state of the suspended
caller.

Do what posix_spawn does: block all signals before the vfork, reset the
handlers to SIG_DFL in the child (keeping the ignored signals ignored,
as CRIU would inherit them otherwise too), and restore the original
signal mask both in the child, before calling libcriu, and in the
parent, once the child is gone.

Signed-off-by: Kir Kolyshkin <[email protected]>
@giuseppe
giuseppe force-pushed the criu-restore-in-child branch from 07dcbe3 to cbd6ca4 Compare September 14, 2026 11:44
@giuseppe

Copy link
Copy Markdown
Member Author

we don't really need the API change if we ``vfork` with detach, an API user can just use detach. So I've dropped that.

I've adjusted your commit message to not refer to the proposed API and amended the following snippet, let me know if it is fine for you:

diff --git a/src/libcrun/criu.c b/src/libcrun/criu.c
index ee5fa985..193fb9f2 100644
--- a/src/libcrun/criu.c
+++ b/src/libcrun/criu.c
@@ -1041,7 +1041,12 @@ criu_restore_child_in_cgroup (const char *cgroup_path, int *criu_ret, libcrun_er
           act.sa_handler = SIG_DFL;
           sigaction (i, &act, NULL);
         }
-      sigprocmask (SIG_SETMASK, &old_mask, NULL);
+      ret = sigprocmask (SIG_SETMASK, &old_mask, NULL);
+      if (UNLIKELY (ret < 0))
+        {
+          crun_make_error (err, errno, "sigprocmask");
+          _safe_exit (EXIT_FAILURE);
+        }
 
       if (! is_empty_string (cgroup_path))
         {
@@ -1055,7 +1060,17 @@ criu_restore_child_in_cgroup (const char *cgroup_path, int *criu_ret, libcrun_er
     }
 
   /* The child is gone by now, it is safe to handle signals again.  */
-  sigprocmask (SIG_SETMASK, &old_mask, NULL);
+  ret = sigprocmask (SIG_SETMASK, &old_mask, NULL);
+  if (UNLIKELY (ret < 0))
+    {
+      int saved_errno = errno;
+
+      /* Do not leave the child behind, and do not lose an error it left.  */
+      waitpid_ignore_stopped (pid, NULL, 0);
+      if (*err != NULL)
+        return -1;
+      return crun_make_error (err, saved_errno, "sigprocmask");
+    }
 
   ret = waitpid_ignore_stopped (pid, &wait_status, 0);
   if (UNLIKELY (ret < 0))

@kolyshkin

Copy link
Copy Markdown
Collaborator

I've adjusted your commit message to not refer to the proposed API and amended the following snippet, let me know if it is fine for you:

It looks like sigprocmask(SIG_SETMASK, &oldmask, NULL) can't really fail here because we're sure the mask is valid (we got it from the kernel) so the added checks are redundant; feel free to either remove them (for simpler code) or keep them (for more correctness, although it seems theoretical). I'm fine either way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants