Skip to content

criu: restore nested PID namespace hierarchies correctly - #21

Open
yiying-zhang wants to merge 11 commits into
gensee-tclonefrom
fix/nested-pidns-restore
Open

criu: restore nested PID namespace hierarchies correctly#21
yiying-zhang wants to merge 11 commits into
gensee-tclonefrom
fix/nested-pidns-restore

Conversation

@yiying-zhang

Copy link
Copy Markdown

Summary

Fix CRIU restoration for workloads containing nested PID namespaces, including cases where the nested namespace’s init process—PID 1—is already a zombie.

The current gensee-tclone implementation preserves the dumped PID hierarchy with --keep-pid-hierarchy, then tries to remove outer levels piecemeal during Phase B. This produces inconsistent PID chains: some process and thread restore paths retain dumped outer/host namespace IDs, while others use only the local ID.

In the reproduced workload, CRIU consequently calls clone3() with set_tid=[1, 6] after PID 6 is already occupied, causing restoration to fail with EEXIST.

Changes

  • Stop passing --keep-pid-hierarchy during tfork restore.
  • Let CRIU’s existing new_pid_ns_truncate_pid_hierarchy() remove only the PID namespace levels outside the restore target.
  • Remove the tfork-specific process and thread PID truncation workarounds.
  • Use the normalized PID hierarchy consistently for clone3(set_tid).
  • Preserve the parallel-sibling restoration flags, including CLONE_PARENT.
  • Record a zombie child using the PID visible in the waiting parent’s namespace.
  • Reconstruct CLONE_NEWPID for a zombie namespace init when its missing task IDs image prevents get_clone_mask() from inferring it.
  • Add a ZDTM regression test for restoring and reaping a zombie PID 1 in a nested PID namespace.

The two CRIU fix commits retain the original student author attribution.

Zombie observer PID

CRIU stores PID namespace values with:

  • pid.ns[0]: PID in the innermost namespace
  • Higher indexes: PIDs in progressively outer namespaces

The PID visible to an ancestor observer is selected with:

pi->pid->ns[
    pi->pid->ns_level - observer->pid->ns_level
].ns_pid

For an outer parent at namespace level 1 observing an init process at level 2, this selects index 1: the PID visible in the outer namespace.

Using localpid(pi) instead selects index 0, which is PID 1 inside the nested namespace. The outer parent cannot use that PID to track or reap the zombie.

Why CLONE_NEWPID must be repaired

CRIU normally derives namespace creation flags from the task’s IDs image.

A zombie namespace init does not have that image, so get_clone_mask() cannot infer that the process must be created with CLONE_NEWPID. The restore path now detects a non-root process whose innermost PID is 1 and restores the missing flag.

Validation

Test host:

Linux 7.0.1-filecowfix-pgcachecow #4 x86_64

Build:

make -j8 zdtm

Result: passed.

Negative control

Current gensee-tclone plus only the new regression test:

./test/zdtm.py run \
  -t zdtm/static/zombie_pidns_init \
  -f ns \
  --ignore-taint

Fails during restore as expected:

clone3 nested set_tid failed flags=0x0 size=2 tids=1/6: File exists
Restoring FAILED

Fixed branch

The same focused test passes:

zdtm/static/zombie_pidns_init PASS

Existing compatibility tests also pass:

zdtm/static/zombie00 PASS
zdtm/static/helper_zombie_child PASS

@yiying-zhang

Copy link
Copy Markdown
Author

Reviewed all three commits — the direction is right: replacing the piecemeal
--keep-pid-hierarchy truncation with CRIU's native normalization removes the
inconsistency that caused the set_tid=[1,6]EEXIST failure. The zombie
observer-PID translation, the generalized CLONE_NEWPID repair, and the argv
resizing all check out. No blocking issues.

One validation gap to close before merge

Dropping the thread set_tid_size = 1 hack in restorer.c is the only change I
can't fully sign off from the diff alone. That hack specifically prevented
concurrent n-copy restores from colliding on kernel-allocated outer-namespace
TIDs. The fix relies on normalization making every restored TID namespace-local
(each copy under its own CLONE_NEWPID root), which should hold — but the new
ZDTM test covers only a single-copy zombie init. The exact case the hack
guarded, N>1 copies + a multi-threaded process in a nested pidns, isn't
exercised. Please add/point to a run of that combination; a regression there
would surface as a hard-to-reproduce EEXIST under concurrency.

Minor / non-blocking

  • item->pid->real = ret narrowing to item == root_item looks correct —
    non-root real PIDs propagate via the shared struct pid self-write before the
    pre-attach_to_tasks barrier — but it now depends entirely on that path.
    Worth a one-line comment noting the parent-side write is intentionally
    root-only.
  • The diff carries the parallel-sibling context (fork_with_pid_mode(..., parallel_sibling), CLONE_PARENT), so this depends on that work already
    being in the base. Since the two now interact, a combined
    parallel-siblings + nested-pidns test would be valuable.

Otherwise LGTM.

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