From 3a6be01e192ed1587cc61b07954c5209cea055c5 Mon Sep 17 00:00:00 2001 From: Arian Boukani Date: Fri, 25 Sep 2026 05:53:48 -0400 Subject: [PATCH] cli/context/store: fix Export writing a truncated tar archive Export deferred tw.Close() before writer.Close(). Deferred calls run last-in, first-out, so the pipe was closed first, and tw.Close() failed with io.ErrClosedPipe before it could write the padding after the last file and the end-of-archive marker. The error was ignored. "docker context import" still reads these archives, because Go's archive/tar stops at EOF. Stricter readers don't: with TLS files in the context, Python's tarfile rejects the export, and so does the macOS tar when reading it from a pipe. Close the tar writer before closing the pipe, and pass its error to CloseWithError instead of dropping it. Signed-off-by: Arian Boukani --- cli/context/store/store.go | 7 +++++-- cli/context/store/store_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 4122e077927f..0d43a849c259 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -264,8 +264,11 @@ func Export(name string, s Reader) io.ReadCloser { reader, writer := io.Pipe() go func() { tw := tar.NewWriter(writer) - defer tw.Close() - defer writer.Close() + defer func() { + // Close the tar writer first, so that the padding and the + // end-of-archive marker are written before the pipe is closed. + writer.CloseWithError(tw.Close()) + }() meta, err := s.GetMetadata(name) if err != nil { writer.CloseWithError(err) diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index 25bfd87c75c0..9640fc7c59ac 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -113,6 +113,35 @@ func TestRemove(t *testing.T) { assert.Equal(t, 0, len(f)) } +func TestExportWritesCompleteArchive(t *testing.T) { + s := New(t.TempDir(), testCfg) + err := s.CreateOrUpdate( + Metadata{ + Endpoints: map[string]any{ + "ep1": endpoint{Foo: "bar"}, + }, + Metadata: context{Bar: "baz"}, + Name: "source", + }) + assert.NilError(t, err) + // "test-data" doesn't fill its 512-byte block, so the archive needs + // padding after it. + assert.NilError(t, s.ResetEndpointTLSMaterial("source", "ep1", &EndpointTLSData{ + Files: map[string][]byte{ + "file1": []byte("test-data"), + }, + })) + + r := Export("source", s) + defer r.Close() + data, err := io.ReadAll(r) + assert.NilError(t, err) + + // A tar archive is made of 512-byte blocks, and ends with two blocks of zeros. + assert.Check(t, is.Equal(len(data)%512, 0)) + assert.Check(t, bytes.HasSuffix(data, make([]byte, 2*512)), "missing end-of-archive marker") +} + func TestListEmptyStore(t *testing.T) { result, err := New(t.TempDir(), testCfg).List() assert.NilError(t, err)