Problem
The second criu-v2 capture on a given pod fails. The first one succeeds and
silently corrupts the container, so the failure lands on the next attempt with
no obvious connection to what caused it:
Error (criu/image.c:748): Unable to open filelocks.img: No such file or directory
The image that fails is whichever CRIU happens to write first, so the reported
error varies.
Cause
After the dump, the agent harvests CRIU's images from the container by moving
them out of, and then removing, the container's overlayfs upperdir directly on
the host:
imgsHost := filepath.Join(sourceUpperdir, "opt/nvsnap-imgs")
moveDirContents(imgsHost, checkpointDir)
os.RemoveAll(imgsHost)
Modifying a mounted overlay's upperdir out of band is undefined behavior. See
Documentation/filesystems/overlayfs.rst, "Changes to underlying filesystems":
the kernel caches its own dentries for the merged view and does not expect the
lower or upper layers to move underneath it.
The observable result is a zombie directory. It still lists, but with
st_nlink == 0, and every openat inside it returns ENOENT:
11836131181 drwxr-xr-x 0 root root 6 /opt/nvsnap-imgs <- poisoned
21506294536 drwxr-xr-x 2 nim nim 32768 /tmp/imgs-test <- healthy
It is not repairable from inside the container: mkdir through the overlay
inherits the poisoned dentry and produces another nlink 0 directory. Only
restarting the container clears it.
Confirmed by running the identical CRIU argv against the same live pod with
only -D changed. Pointed at the poisoned path it fails as above; pointed at
/tmp/imgs-test it returns rc=0 and writes 949 images.
Why it was not caught
test-e2e.sh deploys a fresh pod for every run, so it only ever exercises the
first capture. Any flow that captures twice from one pod hits this, including
re-capture of a long-lived warm pod and iterative debugging against a single
pod.
Fix
Harvest through /proc/<pid>/root whenever the tree is still alive, so all
access goes through the overlay rather than around it. Fall back to the
upperdir only when the tree is gone, which is the successful
non-leave-running case where the container is being torn down anyway and the
mount no longer exists to corrupt.
Related
Two diagnosability defects in the same path made this take far longer to find
than it should have, and are worth keeping fixed:
- The failure path ran
os.RemoveAll unconditionally before reporting, so a
failed harvest deleted dump.log, the only record of why the dump failed.
The error then read dump.log tail: (no dump.log).
moveErr was never surfaced when runErr was also set, making "the dump
wrote no log" and "the harvest destroyed the log" indistinguishable.
Acceptance criteria
- Two consecutive captures against the same pod both succeed.
- A regression test covers the repeat-capture case, which the current e2e
structurally cannot.
Problem
The second criu-v2 capture on a given pod fails. The first one succeeds and
silently corrupts the container, so the failure lands on the next attempt with
no obvious connection to what caused it:
The image that fails is whichever CRIU happens to write first, so the reported
error varies.
Cause
After the dump, the agent harvests CRIU's images from the container by moving
them out of, and then removing, the container's overlayfs upperdir directly on
the host:
Modifying a mounted overlay's upperdir out of band is undefined behavior. See
Documentation/filesystems/overlayfs.rst, "Changes to underlying filesystems":
the kernel caches its own dentries for the merged view and does not expect the
lower or upper layers to move underneath it.
The observable result is a zombie directory. It still lists, but with
st_nlink == 0, and everyopenatinside it returns ENOENT:It is not repairable from inside the container:
mkdirthrough the overlayinherits the poisoned dentry and produces another
nlink 0directory. Onlyrestarting the container clears it.
Confirmed by running the identical CRIU argv against the same live pod with
only
-Dchanged. Pointed at the poisoned path it fails as above; pointed at/tmp/imgs-testit returns rc=0 and writes 949 images.Why it was not caught
test-e2e.shdeploys a fresh pod for every run, so it only ever exercises thefirst capture. Any flow that captures twice from one pod hits this, including
re-capture of a long-lived warm pod and iterative debugging against a single
pod.
Fix
Harvest through
/proc/<pid>/rootwhenever the tree is still alive, so allaccess goes through the overlay rather than around it. Fall back to the
upperdir only when the tree is gone, which is the successful
non-leave-running case where the container is being torn down anyway and the
mount no longer exists to corrupt.
Related
Two diagnosability defects in the same path made this take far longer to find
than it should have, and are worth keeping fixed:
os.RemoveAllunconditionally before reporting, so afailed harvest deleted
dump.log, the only record of why the dump failed.The error then read
dump.log tail: (no dump.log).moveErrwas never surfaced whenrunErrwas also set, making "the dumpwrote no log" and "the harvest destroyed the log" indistinguishable.
Acceptance criteria
structurally cannot.