Problem
criu-v2 restore rebuilds the process tree at the exact PIDs recorded in the
dump, using clone3 with set_tid. It restores into the placeholder pod's
existing PID namespace, so it needs every one of those PIDs to be unused. When
one is not, restore dies at the first collision:
Error (criu/cr-restore.c:1242): Can't fork for 336: File exists
Nothing guarantees they are free. The dump does not serialize a PID namespace,
which the restore log states plainly:
So correctness here rests on a coincidence: that whatever the placeholder and
the agent have allocated does not overlap the dumped tree's PID range.
Current mitigation
#893 pushes the placeholder's next PID allocation to 100000 before
CRIU runs (reservePlaceholderPIDs, internal/agent/restore_v2.go), which the
agent does from the host because a container cannot write
/proc/sys/kernel/ns_last_pid at all. That removes the realistic collision
source and NIM restores reliably with it.
It is still a mitigation. It narrows the window rather than closing it:
- It is best-effort. If the bump fails the restore proceeds unprotected, by
design, so that a failure here cannot break restores that would have worked.
- It assumes no dumped PID exceeds the floor. True today, but it is an
assumption about workload shape, not an invariant.
- Anything that allocates in that namespace between the bump and the clone3 can
still take a PID.
Proposed fix
Dump and restore the PID namespace, so CRIU creates a fresh one and every PID
in it is available by construction. The exact-PID requirement stops being a
constraint on the environment and becomes an internal detail.
This needs design work rather than a flag:
- The placeholder currently provides the namespace CRIU restores into. With a
restored PID namespace, what the placeholder is for, and what pid 1 becomes,
both change.
- Kubernetes and containerd have their own view of the container's init
process. A restored PID namespace has to stay consistent with what the
runtime believes it is supervising, or lifecycle and reaping break.
- The cgroup placement work (clone3 CLONE_INTO_CGROUP, see restore_v2.go)
interacts with this and should be revisited alongside it.
Why it matters beyond the current failure
The risk scales with how many processes a workload starts before it settles,
because that determines how high its session leader PID sits. NIM's entrypoint
chain puts it at 336; vLLM and SGLang land far lower. Larger multi-process
serving stacks will sit higher still, so this becomes more likely as workloads
get bigger, not less.
Acceptance criteria
- A criu-v2 restore succeeds without depending on the target namespace's PID
allocation state.
- The
ns_last_pid reservation can be removed, or is demonstrably redundant.
- A regression test restores a tree whose PIDs are deliberately occupied in the
target namespace beforehand.
References
Problem
criu-v2 restore rebuilds the process tree at the exact PIDs recorded in the
dump, using clone3 with set_tid. It restores into the placeholder pod's
existing PID namespace, so it needs every one of those PIDs to be unused. When
one is not, restore dies at the first collision:
Nothing guarantees they are free. The dump does not serialize a PID namespace,
which the restore log states plainly:
So correctness here rests on a coincidence: that whatever the placeholder and
the agent have allocated does not overlap the dumped tree's PID range.
Current mitigation
#893 pushes the placeholder's next PID allocation to 100000 before
CRIU runs (
reservePlaceholderPIDs, internal/agent/restore_v2.go), which theagent does from the host because a container cannot write
/proc/sys/kernel/ns_last_pidat all. That removes the realistic collisionsource and NIM restores reliably with it.
It is still a mitigation. It narrows the window rather than closing it:
design, so that a failure here cannot break restores that would have worked.
assumption about workload shape, not an invariant.
still take a PID.
Proposed fix
Dump and restore the PID namespace, so CRIU creates a fresh one and every PID
in it is available by construction. The exact-PID requirement stops being a
constraint on the environment and becomes an internal detail.
This needs design work rather than a flag:
restored PID namespace, what the placeholder is for, and what pid 1 becomes,
both change.
process. A restored PID namespace has to stay consistent with what the
runtime believes it is supervising, or lifecycle and reaping break.
interacts with this and should be revisited alongside it.
Why it matters beyond the current failure
The risk scales with how many processes a workload starts before it settles,
because that determines how high its session leader PID sits. NIM's entrypoint
chain puts it at 336; vLLM and SGLang land far lower. Larger multi-process
serving stacks will sit higher still, so this becomes more likely as workloads
get bigger, not less.
Acceptance criteria
allocation state.
ns_last_pidreservation can be removed, or is demonstrably redundant.target namespace beforehand.
References