-
Notifications
You must be signed in to change notification settings - Fork 667
driver: use stdlib tar for docker-container config files #3996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package docker | ||
|
|
||
| import ( | ||
| "archive/tar" | ||
| "bytes" | ||
| "io" | ||
| "io/fs" | ||
| "maps" | ||
| "path" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/docker/buildx/util/confutil" | ||
| "github.com/pkg/errors" | ||
| ) | ||
|
|
||
| const ( | ||
| tarDirMode int64 = 0o755 | ||
| tarFileMode int64 = 0o644 | ||
| ) | ||
|
|
||
| type configTarEntry struct { | ||
| name string | ||
| data []byte | ||
| } | ||
|
|
||
| func tarConfigFiles(files map[string][]byte) (io.ReadCloser, error) { | ||
| entries, dirs, err := configTarEntries(path.Base(confutil.DefaultBuildKitConfigDir), files) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var buf bytes.Buffer | ||
| tw := tar.NewWriter(&buf) | ||
| if err := writeConfigTar(tw, dirs, entries); err != nil { | ||
| _ = tw.Close() | ||
| return nil, err | ||
| } | ||
| if err := tw.Close(); err != nil { | ||
| return nil, err | ||
| } | ||
| return io.NopCloser(bytes.NewReader(buf.Bytes())), nil | ||
| } | ||
|
|
||
| func configTarEntries(configDir string, files map[string][]byte) ([]configTarEntry, []string, error) { | ||
| configDir, err := configArchiveRoot(configDir) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| entries := make([]configTarEntry, 0, len(files)) | ||
| dirs := map[string]struct{}{} | ||
| filePaths := make(map[string]struct{}, len(files)) | ||
| if len(files) > 0 { | ||
| dirs[configDir] = struct{}{} | ||
| } | ||
|
|
||
| for _, name := range slices.Sorted(maps.Keys(files)) { | ||
| archivePath, err := configArchivePath(configDir, name) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
| entries = append(entries, configTarEntry{ | ||
| name: archivePath, | ||
| data: files[name], | ||
| }) | ||
| filePaths[archivePath] = struct{}{} | ||
| for dir := path.Dir(archivePath); dir != "."; dir = path.Dir(dir) { | ||
| dirs[dir] = struct{}{} | ||
| } | ||
| } | ||
|
|
||
| for filePath := range filePaths { | ||
| if _, ok := dirs[filePath]; ok { | ||
| return nil, nil, errors.Errorf("config file path %q conflicts with directory", strings.TrimPrefix(filePath, configDir+"/")) | ||
| } | ||
| } | ||
|
|
||
| return entries, slices.Sorted(maps.Keys(dirs)), nil | ||
| } | ||
|
|
||
| func configArchiveRoot(name string) (string, error) { | ||
| if name == "." || strings.Contains(name, "/") || strings.Contains(name, `\`) || strings.ContainsRune(name, 0) || !fs.ValidPath(name) { | ||
| return "", errors.Errorf("invalid config archive root %q", name) | ||
| } | ||
| return name, nil | ||
| } | ||
|
|
||
| func configArchivePath(configDir, name string) (string, error) { | ||
| if strings.Contains(name, `\`) || strings.ContainsRune(name, 0) { | ||
| return "", errors.Errorf("invalid config file path %q", name) | ||
| } | ||
| // Validate the literal slash path. path.Join would clean traversal first. | ||
| archivePath := configDir + "/" + name | ||
| if !fs.ValidPath(archivePath) { | ||
| return "", errors.Errorf("invalid config file path %q", name) | ||
| } | ||
| return archivePath, nil | ||
| } | ||
|
|
||
| func writeConfigTar(tw *tar.Writer, dirs []string, entries []configTarEntry) error { | ||
| for _, dir := range dirs { | ||
| if err := tw.WriteHeader(&tar.Header{ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we force a format format here? I think Go should select the appropriate one, but something to potentially be aware of.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think leaving |
||
| Name: dir + "/", | ||
| Typeflag: tar.TypeDir, | ||
| Mode: tarDirMode, | ||
| }); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| for _, entry := range entries { | ||
| if err := tw.WriteHeader(&tar.Header{ | ||
| Name: entry.name, | ||
| Typeflag: tar.TypeReg, | ||
| Mode: tarFileMode, | ||
| Size: int64(len(entry.data)), | ||
| }); err != nil { | ||
| return err | ||
| } | ||
| if len(entry.data) == 0 { | ||
| continue | ||
| } | ||
| if _, err := tw.Write(entry.data); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| package docker | ||
|
|
||
| import ( | ||
| "archive/tar" | ||
| "io" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| type tarEntry struct { | ||
| header *tar.Header | ||
| body []byte | ||
| } | ||
|
|
||
| func TestTarConfigFiles(t *testing.T) { | ||
| files := map[string][]byte{ | ||
| "buildkitd.toml": []byte("debug = true\n"), | ||
| "certs/example.com/ca.pem": []byte("certificate"), | ||
| } | ||
|
|
||
| rc, err := tarConfigFiles(files) | ||
| require.NoError(t, err) | ||
| defer rc.Close() | ||
|
|
||
| expected := []string{ | ||
| "buildkit/", | ||
| "buildkit/certs/", | ||
| "buildkit/certs/example.com/", | ||
| "buildkit/buildkitd.toml", | ||
| "buildkit/certs/example.com/ca.pem", | ||
| } | ||
| entries, names := readTarEntries(t, rc) | ||
| require.Equal(t, expected, names) | ||
| require.Len(t, entries, len(expected)) | ||
| for _, name := range expected { | ||
| entry, ok := entries[name] | ||
| require.Truef(t, ok, "missing archive entry %q", name) | ||
| require.Equal(t, 0, entry.header.Uid) | ||
| require.Equal(t, 0, entry.header.Gid) | ||
| require.Empty(t, entry.header.Uname) | ||
| require.Empty(t, entry.header.Gname) | ||
| } | ||
|
|
||
| require.Equal(t, byte(tar.TypeDir), entries["buildkit/"].header.Typeflag) | ||
| require.Equal(t, tarDirMode, entries["buildkit/"].header.Mode) | ||
| require.Equal(t, files["buildkitd.toml"], entries["buildkit/buildkitd.toml"].body) | ||
| require.Equal(t, tarFileMode, entries["buildkit/buildkitd.toml"].header.Mode) | ||
| require.Equal(t, files["certs/example.com/ca.pem"], entries["buildkit/certs/example.com/ca.pem"].body) | ||
| } | ||
|
|
||
| func TestTarConfigFilesSnapshotsContent(t *testing.T) { | ||
| files := map[string][]byte{ | ||
| "buildkitd.toml": []byte("debug = true\n"), | ||
| } | ||
|
|
||
| rc, err := tarConfigFiles(files) | ||
| require.NoError(t, err) | ||
| defer rc.Close() | ||
|
|
||
| files["buildkitd.toml"][0] = '#' | ||
|
|
||
| entries, _ := readTarEntries(t, rc) | ||
| require.Equal(t, []byte("debug = true\n"), entries["buildkit/buildkitd.toml"].body) | ||
| } | ||
|
|
||
| func TestTarConfigFilesEmpty(t *testing.T) { | ||
| rc, err := tarConfigFiles(nil) | ||
| require.NoError(t, err) | ||
| defer rc.Close() | ||
|
|
||
| tr := tar.NewReader(rc) | ||
| _, err = tr.Next() | ||
| require.ErrorIs(t, err, io.EOF) | ||
| } | ||
|
|
||
| func TestTarConfigFilesRejectsInvalidPaths(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| path string | ||
| }{ | ||
| {name: "empty", path: ""}, | ||
| {name: "dot", path: "."}, | ||
| {name: "leading-dot", path: "./buildkitd.toml"}, | ||
| {name: "parent", path: "../buildkitd.toml"}, | ||
| {name: "absolute", path: "/buildkitd.toml"}, | ||
| {name: "trailing-slash", path: "certs/"}, | ||
| {name: "parent-element", path: "certs/../ca.pem"}, | ||
| {name: "dot-element", path: "certs/./ca.pem"}, | ||
| {name: "empty-element", path: "certs//ca.pem"}, | ||
| {name: "backslash", path: `certs\ca.pem`}, | ||
| {name: "nul", path: "certs/ca\x00.pem"}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| rc, err := tarConfigFiles(map[string][]byte{ | ||
| tc.path: []byte("invalid"), | ||
| }) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "invalid config file path") | ||
| require.Nil(t, rc) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestTarConfigFilesRejectsFileDirectoryConflict(t *testing.T) { | ||
| rc, err := tarConfigFiles(map[string][]byte{ | ||
| "certs": []byte("file"), | ||
| "certs/ca.pem": []byte("ca"), | ||
| }) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "conflicts with directory") | ||
| require.Nil(t, rc) | ||
| } | ||
|
|
||
| func TestConfigTarEntriesRejectsInvalidRoot(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| root string | ||
| }{ | ||
| {name: "empty", root: ""}, | ||
| {name: "dot", root: "."}, | ||
| {name: "parent", root: "../buildkit"}, | ||
| {name: "absolute", root: "/buildkit"}, | ||
| {name: "nested", root: "etc/buildkit"}, | ||
| {name: "backslash", root: `etc\buildkit`}, | ||
| {name: "nul", root: "build\x00kit"}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| entries, dirs, err := configTarEntries(tc.root, map[string][]byte{ | ||
| "buildkitd.toml": []byte("debug = true\n"), | ||
| }) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "invalid config archive root") | ||
| require.Nil(t, entries) | ||
| require.Nil(t, dirs) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func readTarEntries(t *testing.T, r io.Reader) (map[string]tarEntry, []string) { | ||
| t.Helper() | ||
|
|
||
| tr := tar.NewReader(r) | ||
| entries := map[string]tarEntry{} | ||
| var names []string | ||
| for { | ||
| hdr, err := tr.Next() | ||
| if err == io.EOF { | ||
| break | ||
| } | ||
| require.NoError(t, err) | ||
| names = append(names, hdr.Name) | ||
| body, err := io.ReadAll(tr) | ||
| require.NoError(t, err) | ||
| entries[hdr.Name] = tarEntry{ | ||
| header: new(*hdr), | ||
| body: body, | ||
| } | ||
| } | ||
| return entries, names | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
configDir is the only name reaching writeConfigTar without passing configArchivePath. It's safe today only because len(files) > 0 makes it a prefix of every validated path. Would it be worth asserting it explicitly as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, fair point.
configDiris currently derived frompath.Base(confutil.DefaultBuildKitConfigDir), so it'sbuildkittoday, but it's still the one path component added to the archive before going through the same explicit validation path. I will tighten that so the archive root is validated too, or refactor it so we don't pass an arbitrary root string around.