Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ go_binary(

go_test(
name = "restore-entrypoint_test",
srcs = ["cold_start_fallback_test.go"],
srcs = [
"cold_start_fallback_test.go",
"pid_reserve_test.go",
],
embed = [":restore-entrypoint_lib"],
)
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ func main() {
}
fmt.Println("=== NVSNAP Restore Entrypoint (go-criu) ===")

// Before anything else forks: push this pod's pid allocations clear of the
// range the checkpoint captured.
reservePIDRange()

// OpenTelemetry. No-op when OTEL_EXPORTER_OTLP_ENDPOINT is unset.
// When the agent sets OTEL_TRACE_PARENT in the placeholder pod env
// (see internal/agent/restore.go), our spans nest under the agent's
Expand Down Expand Up @@ -3547,3 +3551,57 @@ func discoverNvidiaMajors() map[uint32]bool {
}
return majors
}

// pidReserveFloor is the value written to ns_last_pid so this pod's own
// processes allocate above anything a checkpoint is likely to contain. It must
// stay above the agent's acceptance floor (reservedPIDFloor in
// internal/agent/restore_v2.go), which refuses to restore into a pod that
// skipped this step.
const pidReserveFloor = "100000"

// reservePIDRange pushes this pid namespace's next allocation clear of the
// range a checkpoint captured.
//
// CRIU recreates a dumped tree at its exact original pids -- they are baked
// into the image (cached getpid, pthread TCBs, robust futex lists, file lock
// owners), so it cannot renumber them. Any long-lived process this pod starts
// before the restore therefore has to land somewhere the checkpoint does not
// need, or the restore dies with:
//
// Error (criu/cr-restore.c:1242): Can't fork for 363: File exists
//
// Doing it here rather than in the pod manifest is what makes it work for
// production restores: the webhook rewrites the container command to this
// binary (see internal/webhook/restore_entrypoint.go), so this runs as pid 1
// on every restore pod regardless of what the tenant's own command is. A
// manifest-level bump only ever covered pods whose manifest we wrote.
//
// Ordering is the whole point -- this must run before anything forks, which is
// why it sits at the top of main rather than alongside the restore logic.
//
// Deliberately not fatal. A restore pod that cannot reserve still has the
// cold-start fallback path, and crash-looping here would turn a degraded
// restore into no workload at all. The agent enforces instead: it refuses to
// restore into a pod whose pid range was never pushed up, so a silent failure
// here surfaces as a named error there rather than as the intermittent
// "flakiness" this cost us before.
func reservePIDRange() {
reservePIDRangeAt(nsLastPIDPath)
}

// nsLastPIDPath is the real control file; tests point reservePIDRangeAt at a
// temp file instead.
const nsLastPIDPath = "/proc/sys/kernel/ns_last_pid"

func reservePIDRangeAt(path string) {
if err := os.WriteFile(path, []byte(pidReserveFloor), 0o644); err != nil {
// Loud on purpose. The previous shell version ended in `|| echo`, and
// that swallowed failure is what let a 79% restore failure rate look
// like flakiness for days.
fmt.Printf("ERROR: could not reserve pid range: write %s: %v\n", path, err)
fmt.Println("ERROR: CRIU's exact-pid forks may collide with this pod's own processes;")
fmt.Println("ERROR: the agent will refuse the restore rather than fail in the middle of it.")
return
}
fmt.Printf("Reserved pid range: ns_last_pid=%s (own processes allocate above it)\n", pidReserveFloor)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"os"
"path/filepath"
"strconv"
"testing"
)

func TestReservePIDRangeWritesTheFloor(t *testing.T) {
path := filepath.Join(t.TempDir(), "ns_last_pid")
reservePIDRangeAt(path)

got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("control file not written: %v", err)
}
if string(got) != pidReserveFloor {
t.Errorf("wrote %q, want %q", got, pidReserveFloor)
}
}

// A write failure must not take the process down: the restore pod still has a
// cold-start fallback, and crash-looping here would turn a degraded restore
// into no workload at all. The agent refuses the restore instead.
func TestReservePIDRangeSurvivesAnUnwritablePath(t *testing.T) {
dir := t.TempDir()
// A directory cannot be written as a file, which is the closest stand-in
// for the read-only /proc/sys the old shell version believed it faced.
reservePIDRangeAt(dir)
}

// The floor this binary writes must clear the floor the agent accepts,
// otherwise a correctly-reserved pod would still be refused. These constants
// live in different packages and nothing but this test ties them together.
func TestReserveFloorClearsTheAgentAcceptanceFloor(t *testing.T) {
// Mirrors reservedPIDFloor in internal/agent/restore_v2.go. If that value
// changes, this test should fail and force the pair to be reconsidered.
const agentAcceptanceFloor = 50000

got, err := strconv.Atoi(pidReserveFloor)
if err != nil {
t.Fatalf("pidReserveFloor %q is not a number: %v", pidReserveFloor, err)
}
if got <= agentAcceptanceFloor {
t.Errorf("reserve floor %d must exceed the agent's acceptance floor %d, "+
"or every reserved pod is refused", got, agentAcceptanceFloor)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,23 @@ spec:
- |
set -e
mkdir -p /var/run/vllm
# NOTE: this pod does NOT reserve its own pid range. Writing
# /proc/sys/kernel/ns_last_pid from a container fails with EPERM
# (/proc/sys is mounted read-only into the pod) even when privileged,
# so the attempt that used to live here never did anything. The agent
# does it from the host instead, before invoking CRIU restore --
# see reservePlaceholderPIDs in internal/agent/restore_v2.go.
# Push this pod's own pid allocations clear of the range the dump
# captured, so CRIU's exact-pid forks find those pids free.
#
# Load-bearing, and quiet when it is missing. Without it the login
# shell forks a few hundred times sourcing profile.d before the tail
# below starts, parking a long-lived process inside the restored pid
# range; CRIU then dies with "Can't fork for <pid>: File exists" on
# most attempts, and passes on the rest, which reads as flakiness
# rather than breakage.
#
# This was deleted once on the belief that the write returns EPERM
# inside a container. It does not: /proc is mounted rw here and the
# write succeeds -- measured, next child landed at pid 100003.
# The agent independently refuses to restore into a placeholder whose
# pid range was never pushed up, so removing this line fails loudly
# instead of silently.
echo 100000 > /proc/sys/kernel/ns_last_pid || echo "(ns_last_pid bump FAILED -- restore will collide)"
# Restored workload stdio is a plain-file fd on /vllm.out (the
# source manifest's setsid convention); surface it via kubelet.
touch /vllm.out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,23 @@ spec:
args:
- |
set -e
# NOTE: this pod does NOT reserve its own pid range. Writing
# /proc/sys/kernel/ns_last_pid from a container fails with EPERM
# (/proc/sys is mounted read-only into the pod) even when privileged,
# so the attempt that used to live here never did anything. The agent
# does it from the host instead, before invoking CRIU restore --
# see reservePlaceholderPIDs in internal/agent/restore_v2.go.
# Push this pod's own pid allocations clear of the range the dump
# captured, so CRIU's exact-pid forks find those pids free.
#
# Load-bearing, and quiet when it is missing. Without it the login
# shell forks a few hundred times sourcing profile.d before the tail
# below starts, parking a long-lived process inside the restored pid
# range; CRIU then dies with "Can't fork for <pid>: File exists" on
# most attempts, and passes on the rest, which reads as flakiness
# rather than breakage.
#
# This was deleted once on the belief that the write returns EPERM
# inside a container. It does not: /proc is mounted rw here and the
# write succeeds -- measured, next child landed at pid 100003.
# The agent independently refuses to restore into a placeholder whose
# pid range was never pushed up, so removing this line fails loudly
# instead of silently.
echo 100000 > /proc/sys/kernel/ns_last_pid || echo "(ns_last_pid bump FAILED -- restore will collide)"
# Restored workload stdio is a plain-file fd on /sglang.out (the
# source manifest's setsid convention); surface it via kubelet.
touch /sglang.out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,23 @@ spec:
args:
- |
set -e
# NOTE: this pod does NOT reserve its own pid range. Writing
# /proc/sys/kernel/ns_last_pid from a container fails with EPERM
# (/proc/sys is mounted read-only into the pod) even when privileged,
# so the attempt that used to live here never did anything. The agent
# does it from the host instead, before invoking CRIU restore --
# see reservePlaceholderPIDs in internal/agent/restore_v2.go.
# Push this pod's own pid allocations clear of the range the dump
# captured, so CRIU's exact-pid forks find those pids free.
#
# Load-bearing, and quiet when it is missing. Without it the login
# shell forks a few hundred times sourcing profile.d before the tail
# below starts, parking a long-lived process inside the restored pid
# range; CRIU then dies with "Can't fork for <pid>: File exists" on
# most attempts, and passes on the rest, which reads as flakiness
# rather than breakage.
#
# This was deleted once on the belief that the write returns EPERM
# inside a container. It does not: /proc is mounted rw here and the
# write succeeds -- measured, next child landed at pid 100003.
# The agent independently refuses to restore into a placeholder whose
# pid range was never pushed up, so removing this line fails loudly
# instead of silently.
echo 100000 > /proc/sys/kernel/ns_last_pid || echo "(ns_last_pid bump FAILED -- restore will collide)"
# Restored workload stdio is a plain-file fd on /tmp/nim.out (the
# source manifest's setsid convention); surface it via kubelet.
#
Expand Down Expand Up @@ -74,6 +85,13 @@ spec:
failureThreshold: 180
securityContext:
privileged: true
# The NIM image's default user is uid 1000, and privileged does not
# confer root. Writing /proc/sys/kernel/ns_last_pid then fails with
# "Operation not permitted", the pid reservation above silently does
# nothing, and the agent refuses the restore. Only the placeholder runs
# as root: CRIU restores the workload with the uid recorded in the
# checkpoint, so this does not change what the workload runs as.
runAsUser: 0
resources:
limits:
nvidia.com/gpu: "1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,23 @@ spec:
args:
- |
set -e
# NOTE: this pod does NOT reserve its own pid range. Writing
# /proc/sys/kernel/ns_last_pid from a container fails with EPERM
# (/proc/sys is mounted read-only into the pod) even when privileged,
# so the attempt that used to live here never did anything. The agent
# does it from the host instead, before invoking CRIU restore --
# see reservePlaceholderPIDs in internal/agent/restore_v2.go.
# Push this pod's own pid allocations clear of the range the dump
# captured, so CRIU's exact-pid forks find those pids free.
#
# Load-bearing, and quiet when it is missing. Without it the login
# shell forks a few hundred times sourcing profile.d before the tail
# below starts, parking a long-lived process inside the restored pid
# range; CRIU then dies with "Can't fork for <pid>: File exists" on
# most attempts, and passes on the rest, which reads as flakiness
# rather than breakage.
#
# This was deleted once on the belief that the write returns EPERM
# inside a container. It does not: /proc is mounted rw here and the
# write succeeds -- measured, next child landed at pid 100003.
# The agent independently refuses to restore into a placeholder whose
# pid range was never pushed up, so removing this line fails loudly
# instead of silently.
echo 100000 > /proc/sys/kernel/ns_last_pid || echo "(ns_last_pid bump FAILED -- restore will collide)"
# Restored workload stdio is a plain-file fd on /sglang.out (the
# source manifest's setsid convention); surface it via kubelet.
touch /sglang.out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,23 @@ spec:
args:
- |
set -e
# NOTE: this pod does NOT reserve its own pid range. Writing
# /proc/sys/kernel/ns_last_pid from a container fails with EPERM
# (/proc/sys is mounted read-only into the pod) even when privileged,
# so the attempt that used to live here never did anything. The agent
# does it from the host instead, before invoking CRIU restore --
# see reservePlaceholderPIDs in internal/agent/restore_v2.go.
# Push this pod's own pid allocations clear of the range the dump
# captured, so CRIU's exact-pid forks find those pids free.
#
# Load-bearing, and quiet when it is missing. Without it the login
# shell forks a few hundred times sourcing profile.d before the tail
# below starts, parking a long-lived process inside the restored pid
# range; CRIU then dies with "Can't fork for <pid>: File exists" on
# most attempts, and passes on the rest, which reads as flakiness
# rather than breakage.
#
# This was deleted once on the belief that the write returns EPERM
# inside a container. It does not: /proc is mounted rw here and the
# write succeeds -- measured, next child landed at pid 100003.
# The agent independently refuses to restore into a placeholder whose
# pid range was never pushed up, so removing this line fails loudly
# instead of silently.
echo 100000 > /proc/sys/kernel/ns_last_pid || echo "(ns_last_pid bump FAILED -- restore will collide)"
# Restored workload stdio is a plain-file fd on /sglang.out (the
# source manifest's setsid convention); surface it via kubelet.
touch /sglang.out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,23 @@ spec:
args:
- |
set -e
# NOTE: this pod does NOT reserve its own pid range. Writing
# /proc/sys/kernel/ns_last_pid from a container fails with EPERM
# (/proc/sys is mounted read-only into the pod) even when privileged,
# so the attempt that used to live here never did anything. The agent
# does it from the host instead, before invoking CRIU restore --
# see reservePlaceholderPIDs in internal/agent/restore_v2.go.
# Push this pod's own pid allocations clear of the range the dump
# captured, so CRIU's exact-pid forks find those pids free.
#
# Load-bearing, and quiet when it is missing. Without it the login
# shell forks a few hundred times sourcing profile.d before the tail
# below starts, parking a long-lived process inside the restored pid
# range; CRIU then dies with "Can't fork for <pid>: File exists" on
# most attempts, and passes on the rest, which reads as flakiness
# rather than breakage.
#
# This was deleted once on the belief that the write returns EPERM
# inside a container. It does not: /proc is mounted rw here and the
# write succeeds -- measured, next child landed at pid 100003.
# The agent independently refuses to restore into a placeholder whose
# pid range was never pushed up, so removing this line fails loudly
# instead of silently.
echo 100000 > /proc/sys/kernel/ns_last_pid || echo "(ns_last_pid bump FAILED -- restore will collide)"
# Restored workload stdio is a plain-file fd on /trtllm.out (the
# source manifest's setsid convention); surface it via kubelet.
touch /trtllm.out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,23 @@ spec:
- |
set -e
mkdir -p /var/run/vllm
# NOTE: this pod does NOT reserve its own pid range. Writing
# /proc/sys/kernel/ns_last_pid from a container fails with EPERM
# (/proc/sys is mounted read-only into the pod) even when privileged,
# so the attempt that used to live here never did anything. The agent
# does it from the host instead, before invoking CRIU restore --
# see reservePlaceholderPIDs in internal/agent/restore_v2.go.
# Push this pod's own pid allocations clear of the range the dump
# captured, so CRIU's exact-pid forks find those pids free.
#
# Load-bearing, and quiet when it is missing. Without it the login
# shell forks a few hundred times sourcing profile.d before the tail
# below starts, parking a long-lived process inside the restored pid
# range; CRIU then dies with "Can't fork for <pid>: File exists" on
# most attempts, and passes on the rest, which reads as flakiness
# rather than breakage.
#
# This was deleted once on the belief that the write returns EPERM
# inside a container. It does not: /proc is mounted rw here and the
# write succeeds -- measured, next child landed at pid 100003.
# The agent independently refuses to restore into a placeholder whose
# pid range was never pushed up, so removing this line fails loudly
# instead of silently.
echo 100000 > /proc/sys/kernel/ns_last_pid || echo "(ns_last_pid bump FAILED -- restore will collide)"
# Restored workload stdio is a plain-file fd on /vllm.out (the
# source manifest's setsid convention); surface it via kubelet.
touch /vllm.out
Expand Down
Loading