Skip to content

nvsnap: second capture on the same pod fails - image harvest corrupts the container's overlay mount #892

Description

@balaji-g

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.

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions