Containerd vulnerable to host filesystem access during image unpack
Description
containerd is a container runtime. A time-of-check to time-of-use (TOCTOU) vulnerability was found in containerd v2.1.0. While unpacking an image during an image pull, specially crafted container images could arbitrarily modify the host file system. The only affected version of containerd is 2.1.0. Other versions of containerd are not affected. This bug has been fixed in containerd 2.1.1. Users should update to this version to resolve the issue. As a workaround, ensure that only trusted images are used and that only trusted users have permissions to import images.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/containerd/containerd/v2Go | >= 2.1.0, < 2.1.1 | 2.1.1 |
Affected products
1- Range: = 2.1.0
Patches
1cada13298fbaMerge commit from fork
1 file changed · +10 −35
pkg/archive/tar.go+10 −35 modified@@ -157,31 +157,6 @@ func Apply(ctx context.Context, root string, r io.Reader, opts ...ApplyOpt) (int return options.applyFunc(ctx, root, r, options) } -// cachedRootPath will memoize root paths, avoiding redundant checks. -type cachedRootPath struct { - root string - cache map[string]string -} - -func newCachedRootPath(root string) *cachedRootPath { - return &cachedRootPath{ - root: root, - cache: make(map[string]string), - } -} - -func (c *cachedRootPath) get(path string) (string, error) { - if hit, ok := c.cache[path]; ok { - return hit, nil - } - p, err := fs.RootPath(c.root, path) - if err != nil { - return "", err - } - c.cache[path] = p - return p, nil -} - // applyNaive applies a tar stream of an OCI style diff tar to a directory // applying each file as either a whole file or whiteout. // See https://github.com/opencontainers/image-spec/blob/main/layer.md#applying-changesets @@ -239,7 +214,6 @@ func applyNaive(ctx context.Context, root string, r io.Reader, options ApplyOpti } // Iterate through the files in the archive. - rootPath := newCachedRootPath(root) for { select { case <-ctx.Done(): @@ -276,7 +250,7 @@ func applyNaive(ctx context.Context, root string, r io.Reader, options ApplyOpti // Split name and resolve symlinks for root directory. ppath, base := filepath.Split(hdr.Name) - ppath, err = rootPath.get(ppath) + ppath, err = fs.RootPath(root, ppath) if err != nil { return 0, fmt.Errorf("failed to get root path: %w", err) } @@ -328,7 +302,7 @@ func applyNaive(ctx context.Context, root string, r io.Reader, options ApplyOpti srcData := io.Reader(tr) srcHdr := hdr - if err := rootPath.createTarFile(ctx, path, srcHdr, srcData, options.NoSameOwner); err != nil { + if err := createTarFile(ctx, path, root, srcHdr, srcData, options.NoSameOwner); err != nil { return 0, err } @@ -341,7 +315,7 @@ func applyNaive(ctx context.Context, root string, r io.Reader, options ApplyOpti } for _, hdr := range dirs { - path, err := rootPath.get(hdr.Name) + path, err := fs.RootPath(root, hdr.Name) if err != nil { return 0, err } @@ -353,7 +327,7 @@ func applyNaive(ctx context.Context, root string, r io.Reader, options ApplyOpti return size, nil } -func (c *cachedRootPath) createTarFile(ctx context.Context, path string, hdr *tar.Header, reader io.Reader, noSameOwner bool) error { +func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader, noSameOwner bool) error { // hdr.Mode is in linux format, which we can use for syscalls, // but for os.Foo() calls we need the mode converted to os.FileMode, // so use hdrInfo.Mode() (they differ for e.g. setuid bits) @@ -397,7 +371,7 @@ func (c *cachedRootPath) createTarFile(ctx context.Context, path string, hdr *ta } case tar.TypeLink: - targetPath, err := c.hardlinkRootPath(hdr.Linkname) + targetPath, err := hardlinkRootPath(extractDir, hdr.Linkname) if err != nil { return err } @@ -792,6 +766,7 @@ func copyBuffered(ctx context.Context, dst io.Writer, src io.Reader) (written in } } return written, err + } // hardlinkRootPath returns target linkname, evaluating and bounding any @@ -804,16 +779,16 @@ func copyBuffered(ctx context.Context, dst io.Writer, src io.Reader) (written in // ln /tmp/xxx /tmp/yyy // // /tmp/yyy should be softlink which be same of /tmp/xxx, not /tmp/zzz. -func (c *cachedRootPath) hardlinkRootPath(linkname string) (string, error) { +func hardlinkRootPath(root, linkname string) (string, error) { ppath, base := filepath.Split(linkname) - ppath, err := c.get(ppath) + ppath, err := fs.RootPath(root, ppath) if err != nil { return "", err } targetPath := filepath.Join(ppath, base) - if !strings.HasPrefix(targetPath, c.root) { - targetPath = c.root + if !strings.HasPrefix(targetPath, root) { + targetPath = root } return targetPath, nil }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-cm76-qm8v-3j95ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-47290ghsaADVISORY
- github.com/containerd/containerd/commit/cada13298fba85493badb6fecb6ccf80e49673ccghsax_refsource_MISCWEB
- github.com/containerd/containerd/releases/tag/v2.1.1ghsax_refsource_MISCWEB
- github.com/containerd/containerd/security/advisories/GHSA-cm76-qm8v-3j95ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.