Description
Since Docker Engine 29.7.0, pulling the public image quay.io/buildah/stable:v1.43.1 fails deterministically during layer extraction:
failed to register layer: mkdirat etc/dnf: no such file or directory
This image pulled fine on the previous engine version (29.6.x). Our CI pulls it many times a day; failures started the day 29.7.0 was published (2026-07-30) on hosts that install the engine from get.docker.com at provisioning time. Nothing else changed — same image digest, same AMI, same kernel.
Reproduce the problem
On a host running 29.7.0:
$ docker pull quay.io/buildah/stable:v1.43.1
v1.43.1: Pulling from buildah/stable
9c7a183a5be5: Pull complete
e1d548ffb6a0: Extracting [==================================================>] 251B/251B
...
failed to register layer: mkdirat etc/dnf: no such file or directory
The failing layer is sha256:e1d548ffb6a01c21199fac9d279812b6ddac622a1806ec589defd29dfef38c0f (251 bytes). Its complete contents:
$ tar tzvf layer.tar.gz
drwxr-xr-x 0 0 0 0 May 31 08:47 etc/dnf/
-rw-r--r-- 0 0 0 131 May 31 17:22 etc/dnf/dnf.conf
The layer has no etc/ entry — the parent directory is implied.
Describe the results you received
docker pull fails with failed to register layer: mkdirat etc/dnf: no such file or directory. The image never becomes available locally. It reproduces on every attempt.
Describe the results you expected
The image pulls successfully, as it did on 29.6.x.
Additional context
The proximate cause looks clear. createImpliedDirectories in moby/go-archive only synthesizes missing parent directories for non-directory entries:
func createImpliedDirectories(root *os.Root, hdr *tar.Header, options *TarOptions) error {
// For non-directory entries, ensure that the parent directory exists.
if hdr.Typeflag != tar.TypeDir {
The first entry in this layer is etc/dnf/, which is a directory entry, so the implied parent etc/ is never created. Extraction then reaches:
case tar.TypeDir:
if fi, err := root.Lstat(dstPath); err != nil || !fi.IsDir() {
if err := root.Mkdir(dstPath, hdrInfo.Mode()&0o777); err != nil {
return err
}
}
root.Mkdir("etc/dnf") with no etc present returns ENOENT, which matches the error exactly.
What I could not determine is why this changed between releases. That Typeflag != tar.TypeDir guard is present in both v0.2.1 and v0.3.0, and both versions create directory entries with a plain (non-recursive) Mkdir, so the guard alone does not explain the regression. 29.7.0 bumped github.com/moby/go-archive to v0.3.0 (for CVE-2026-17106), which reworked Unpack/UnpackLayer onto Go's sandboxed os.Root API:
os.OpenRoot(dest) now wraps the destination; os.Mkdir/os.Lstat/os.RemoveAll became root.Mkdir/root.Lstat/root.RemoveAll
- entry-name normalization changed from
filepath.Clean(hdr.Name) to path.Clean(strings.TrimLeft(hdr.Name, "/")) plus a new filepath.IsLocal(name) rejection
createImpliedDirectories was reimplemented from a single user.MkdirAllAndChown into a per-component root.Mkdir loop, and in Unpack its call site moved from before the Lstat/RemoveAll block to after it
That bump is the only change in 29.7.0 that touches layer unpacking, so the behavioral delta is very likely in there, but it needs a bisect to pin down.
Whether or not a layer whose first entry is a directory with an unlisted parent is considered well-formed, this is a behavior change that breaks pulls of a widely used public image, and buildah/podman-built images produce this layout routinely.
Docker version
Failing: 29.7.0. Last known-good: the engine version our hosts installed before it, 29.6.x (not bisected).
Client: Docker Engine - Community
Version: 29.7.0
API version: 1.55
Go version: go1.26.5
Git commit: c1eba93
Built: Thu Jul 30 20:20:28 2026
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 29.7.0
API version: 1.55 (minimum version 1.40)
Go version: go1.26.5
Git commit: 4b5cb71
Built: Thu Jul 30 20:20:28 2026
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v2.2.6
GitCommit: 11ce9d5f3c68c941867e82890e93e815c1304f1b
runc:
Version: 1.3.6
GitCommit: v1.3.6-0-g491b69ba
docker-init:
Version: 0.19.0
GitCommit: de40ad0
docker info
Server:
Containers: 0
Images: 0
Server Version: 29.7.0
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 11ce9d5f3c68c941867e82890e93e815c1304f1b
runc version: v1.3.6-0-g491b69ba
init version: de40ad0
Security Options:
apparmor
seccomp
Profile: builtin
cgroupns
Kernel Version: 6.17.0-1019-aws
Operating System: Ubuntu 24.04.4 LTS
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 7.656GiB
Live Restore Enabled: false
Description
Since Docker Engine 29.7.0, pulling the public image
quay.io/buildah/stable:v1.43.1fails deterministically during layer extraction:This image pulled fine on the previous engine version (29.6.x). Our CI pulls it many times a day; failures started the day 29.7.0 was published (2026-07-30) on hosts that install the engine from
get.docker.comat provisioning time. Nothing else changed — same image digest, same AMI, same kernel.Reproduce the problem
On a host running 29.7.0:
The failing layer is
sha256:e1d548ffb6a01c21199fac9d279812b6ddac622a1806ec589defd29dfef38c0f(251 bytes). Its complete contents:The layer has no
etc/entry — the parent directory is implied.Describe the results you received
docker pullfails withfailed to register layer: mkdirat etc/dnf: no such file or directory. The image never becomes available locally. It reproduces on every attempt.Describe the results you expected
The image pulls successfully, as it did on 29.6.x.
Additional context
The proximate cause looks clear.
createImpliedDirectoriesinmoby/go-archiveonly synthesizes missing parent directories for non-directory entries:The first entry in this layer is
etc/dnf/, which is a directory entry, so the implied parentetc/is never created. Extraction then reaches:root.Mkdir("etc/dnf")with noetcpresent returns ENOENT, which matches the error exactly.What I could not determine is why this changed between releases. That
Typeflag != tar.TypeDirguard is present in both v0.2.1 and v0.3.0, and both versions create directory entries with a plain (non-recursive)Mkdir, so the guard alone does not explain the regression. 29.7.0 bumpedgithub.com/moby/go-archiveto v0.3.0 (for CVE-2026-17106), which reworkedUnpack/UnpackLayeronto Go's sandboxedos.RootAPI:os.OpenRoot(dest)now wraps the destination;os.Mkdir/os.Lstat/os.RemoveAllbecameroot.Mkdir/root.Lstat/root.RemoveAllfilepath.Clean(hdr.Name)topath.Clean(strings.TrimLeft(hdr.Name, "/"))plus a newfilepath.IsLocal(name)rejectioncreateImpliedDirectorieswas reimplemented from a singleuser.MkdirAllAndChowninto a per-componentroot.Mkdirloop, and inUnpackits call site moved from before theLstat/RemoveAllblock to after itThat bump is the only change in 29.7.0 that touches layer unpacking, so the behavioral delta is very likely in there, but it needs a bisect to pin down.
Whether or not a layer whose first entry is a directory with an unlisted parent is considered well-formed, this is a behavior change that breaks pulls of a widely used public image, and buildah/podman-built images produce this layout routinely.
Docker version
Failing: 29.7.0. Last known-good: the engine version our hosts installed before it, 29.6.x (not bisected).
docker info