From f6f51a8548bbfe8e497795d16acb6608f8f9eac4 Mon Sep 17 00:00:00 2001 From: nrvate <11264848+nrvate@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:22:18 -0500 Subject: [PATCH] build: detect zstd-compressed context from stdin isArchive() decides whether stdin carries a build context or a raw Dockerfile by sniffing magic bytes. The table knows bzip2, gzip, xz and plain tar, but not zstd, so a zstd-compressed context piped to 'docker build -' is parsed as a Dockerfile and the build fails on line 1: $ echo 'FROM scratch' > Dockerfile $ tar cf ctx.tar Dockerfile && zstd -q ctx.tar -o ctx.tar.zst $ docker build --check - < ctx.tar.zst 1 | >>> (\u2572/\u2572d '5 \u2572F`i... gzip and plain tar on the same path work. The daemon already decompresses zstd build contexts - verified against Docker Engine 29.7.2 by building the same tar.zst context through the /build API directly - so only the client-side sniff rejects it today. This adds the zstd frame magic to the table and a unit test covering all supported formats. Related: moby/go-archive#13 Signed-off-by: nrvate <11264848+nrvate@users.noreply.github.com> --- build/utils.go | 1 + build/utils_test.go | 62 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/build/utils.go b/build/utils.go index 2cef546ca0b9..0064223fb672 100644 --- a/build/utils.go +++ b/build/utils.go @@ -29,6 +29,7 @@ func isArchive(header []byte) bool { {0x42, 0x5A, 0x68}, // bzip2 {0x1F, 0x8B, 0x08}, // gzip {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz + {0x28, 0xB5, 0x2F, 0xFD}, // zstd } { if len(header) < len(m) { continue diff --git a/build/utils_test.go b/build/utils_test.go index d4a49ec47bbb..ea4b2610c045 100644 --- a/build/utils_test.go +++ b/build/utils_test.go @@ -1,6 +1,8 @@ package build import ( + "archive/tar" + "bytes" "context" "strings" "testing" @@ -257,3 +259,63 @@ func TestParseResourceLimits(t *testing.T) { require.Error(t, err) }) } + +func TestIsArchive(t *testing.T) { + tarHeader := func() []byte { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + require.NoError(t, tw.WriteHeader(&tar.Header{Name: "Dockerfile", Mode: 0o644, Size: 13})) + _, err := tw.Write([]byte("FROM scratch\n")) + require.NoError(t, err) + require.NoError(t, tw.Close()) + return buf.Bytes() + } + + tests := []struct { + doc string + header []byte + expected bool + }{ + { + doc: "plain tar", + header: tarHeader(), + expected: true, + }, + { + doc: "gzip", + header: []byte{0x1F, 0x8B, 0x08, 0x00}, + expected: true, + }, + { + doc: "bzip2", + header: []byte{0x42, 0x5A, 0x68, 0x31}, + expected: true, + }, + { + doc: "xz", + header: []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, + expected: true, + }, + { + doc: "zstd", + header: []byte{0x28, 0xB5, 0x2F, 0xFD, 0x00}, + expected: true, + }, + { + doc: "dockerfile", + header: []byte("FROM scratch\n"), + expected: false, + }, + { + doc: "truncated zstd magic", + header: []byte{0x28, 0xB5}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.doc, func(t *testing.T) { + require.Equal(t, tt.expected, isArchive(tt.header)) + }) + } +}