diff --git a/bake/bake.go b/bake/bake.go index 8e4077c0a75..10f6cf1e8bc 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -1117,62 +1117,34 @@ func updateContext(t *build.Inputs, inp *Input) { t.ContextState = &st } -// validateContextsEntitlements is a basic check to ensure contexts do not -// escape local directories when loaded from remote sources. This is to be -// replaced with proper entitlements support in the future. -func validateContextsEntitlements(t build.Inputs, inp *Input) error { - if inp == nil || inp.State == nil { - return nil - } - if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok { - if vv, _ := strconv.ParseBool(v); vv { - return nil - } - } +func collectLocalPaths(t build.Inputs) []string { + var out []string if t.ContextState == nil { - if err := checkPath(t.ContextPath); err != nil { - return err + if v, ok := isLocalPath(t.ContextPath); ok { + out = append(out, v) + } + if v, ok := isLocalPath(t.DockerfilePath); ok { + out = append(out, v) } + } else if strings.HasPrefix(t.ContextPath, "cwd://") { + out = append(out, strings.TrimPrefix(t.ContextPath, "cwd://")) } for _, v := range t.NamedContexts { if v.State != nil { continue } - if err := checkPath(v.Path); err != nil { - return err + if v, ok := isLocalPath(v.Path); ok { + out = append(out, v) } } - return nil + return out } -func checkPath(p string) error { +func isLocalPath(p string) (string, bool) { if build.IsRemoteURL(p) || strings.HasPrefix(p, "target:") || strings.HasPrefix(p, "docker-image:") { - return nil - } - p, err := filepath.EvalSymlinks(p) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return err - } - p, err = filepath.Abs(p) - if err != nil { - return err - } - wd, err := os.Getwd() - if err != nil { - return err + return "", false } - rel, err := filepath.Rel(wd, p) - if err != nil { - return err - } - parts := strings.Split(rel, string(os.PathSeparator)) - if parts[0] == ".." { - return errors.Errorf("path %s is outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS=1", p) - } - return nil + return strings.TrimPrefix(p, "cwd://"), true } func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { @@ -1212,9 +1184,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { // it's not outside the working directory and then resolve it to an // absolute path. bi.DockerfilePath = path.Clean(strings.TrimPrefix(bi.DockerfilePath, "cwd://")) - if err := checkPath(bi.DockerfilePath); err != nil { - return nil, err - } var err error bi.DockerfilePath, err = filepath.Abs(bi.DockerfilePath) if err != nil { @@ -1251,10 +1220,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { } } - if err := validateContextsEntitlements(bi, inp); err != nil { - return nil, err - } - t.Context = &bi.ContextPath args := map[string]string{} @@ -1315,6 +1280,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { if err != nil { return nil, err } + bo.SecretSpecs = secrets + secretAttachment, err := controllerapi.CreateSecrets(secrets) if err != nil { return nil, err @@ -1328,6 +1295,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { if len(sshSpecs) == 0 && (buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL))) { sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"}) } + bo.SSHSpecs = sshSpecs + sshAttachment, err := controllerapi.CreateSSH(sshSpecs) if err != nil { return nil, err diff --git a/bake/entitlements.go b/bake/entitlements.go index 030e812beda..8fe8b5b634f 100644 --- a/bake/entitlements.go +++ b/bake/entitlements.go @@ -2,15 +2,21 @@ package bake import ( "bufio" + "cmp" "context" "fmt" "io" + "io/fs" "os" + "path/filepath" "slices" + "strconv" "strings" + "syscall" "github.com/containerd/console" "github.com/docker/buildx/build" + "github.com/docker/buildx/util/osutil" "github.com/moby/buildkit/util/entitlements" "github.com/pkg/errors" ) @@ -67,10 +73,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) { conf.ImagePush = append(conf.ImagePush, v) conf.ImageLoad = append(conf.ImageLoad, v) default: - return conf, errors.Errorf("uknown entitlement key %q", k) + return conf, errors.Errorf("unknown entitlement key %q", k) } - - // TODO: dedupe slices and parent paths } } return conf, nil @@ -101,10 +105,73 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro } } } + + rwPaths := map[string]struct{}{} + roPaths := map[string]struct{}{} + + for _, p := range collectLocalPaths(bo.Inputs) { + roPaths[p] = struct{}{} + } + + for _, out := range bo.Exports { + if out.Type == "local" { + if dest, ok := out.Attrs["dest"]; ok { + rwPaths[dest] = struct{}{} + } + } + if out.Type == "tar" { + if dest, ok := out.Attrs["dest"]; ok && dest != "-" { + rwPaths[dest] = struct{}{} + } + } + } + + for _, ce := range bo.CacheTo { + if ce.Type == "local" { + if dest, ok := ce.Attrs["dest"]; ok { + rwPaths[dest] = struct{}{} + } + } + } + + for _, ci := range bo.CacheFrom { + if ci.Type == "local" { + if src, ok := ci.Attrs["src"]; ok { + roPaths[src] = struct{}{} + } + } + } + + for _, secret := range bo.SecretSpecs { + if secret.FilePath != "" { + roPaths[secret.FilePath] = struct{}{} + } + } + + for _, ssh := range bo.SSHSpecs { + for _, p := range ssh.Paths { + roPaths[p] = struct{}{} + } + if len(ssh.Paths) == 0 { + expected.SSH = true + } + } + + var err error + expected.FSRead, err = findMissingPaths(c.FSRead, roPaths) + if err != nil { + return err + } + + expected.FSWrite, err = findMissingPaths(c.FSWrite, rwPaths) + if err != nil { + return err + } + return nil } -func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error { +func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Writer) error { var term bool if _, err := console.ConsoleFromFile(os.Stdin); err == nil { term = true @@ -113,32 +180,78 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error { var msgs []string var flags []string + // these warnings are currently disabled to give users time to update + var msgsFS []string + var flagsFS []string + if c.NetworkHost { msgs = append(msgs, " - Running build containers that can access host network") - flags = append(flags, "network.host") + flags = append(flags, string(EntitlementKeyNetworkHost)) } if c.SecurityInsecure { msgs = append(msgs, " - Running privileged containers that can make system changes") - flags = append(flags, "security.insecure") + flags = append(flags, string(EntitlementKeySecurityInsecure)) + } + + if c.SSH { + msgsFS = append(msgsFS, " - Forwarding default SSH agent socket") + flagsFS = append(flagsFS, string(EntitlementKeySSH)) + } + + roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite) + wd, err := os.Getwd() + if err != nil { + return errors.Wrap(err, "failed to get current working directory") + } + wd, err = filepath.EvalSymlinks(wd) + if err != nil { + return errors.Wrap(err, "failed to evaluate working directory") + } + roPaths = toRelativePaths(roPaths, wd) + rwPaths = toRelativePaths(rwPaths, wd) + commonPaths = toRelativePaths(commonPaths, wd) + + if len(commonPaths) > 0 { + for _, p := range commonPaths { + msgsFS = append(msgsFS, fmt.Sprintf(" - Read and write access to path %s", p)) + flagsFS = append(flagsFS, string(EntitlementKeyFS)+"="+p) + } + } + + if len(roPaths) > 0 { + for _, p := range roPaths { + msgsFS = append(msgsFS, fmt.Sprintf(" - Read access to path %s", p)) + flagsFS = append(flagsFS, string(EntitlementKeyFSRead)+"="+p) + } + } + + if len(rwPaths) > 0 { + for _, p := range rwPaths { + msgsFS = append(msgsFS, fmt.Sprintf(" - Write access to path %s", p)) + flagsFS = append(flagsFS, string(EntitlementKeyFSWrite)+"="+p) + } } - if len(msgs) == 0 { + if len(msgs) == 0 && len(msgsFS) == 0 { return nil } fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n") - for _, m := range msgs { + for _, m := range slices.Concat(msgs, msgsFS) { fmt.Fprintf(out, "%s\n", m) } for i, f := range flags { flags[i] = "--allow=" + f } + for i, f := range flagsFS { + flagsFS[i] = "--allow=" + f + } if term { - fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " ")) + fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " ")) } else { - fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " ")) + fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " ")) } args := append([]string(nil), os.Args...) @@ -149,7 +262,35 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error { if idx != -1 { fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n") - fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " ")) + fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(slices.Concat(flags, flagsFS), " "), strings.Join(args[idx+1:], " ")) + } + + fsEntitlementsEnabled := false + if isRemote { + if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok { + vv, err := strconv.ParseBool(v) + if err != nil { + return errors.Wrapf(err, "failed to parse BAKE_ALLOW_REMOTE_FS_ACCESS value %q", v) + } + fsEntitlementsEnabled = !vv + } else { + fsEntitlementsEnabled = true + } + } + v, fsEntitlementsSet := os.LookupEnv("BUILDX_BAKE_ENTITLEMENTS_FS") + if fsEntitlementsSet { + vv, err := strconv.ParseBool(v) + if err != nil { + return errors.Wrapf(err, "failed to parse BUILDX_BAKE_ENTITLEMENTS_FS value %q", v) + } + fsEntitlementsEnabled = vv + } + + if !fsEntitlementsEnabled && len(msgs) == 0 { + if !fsEntitlementsSet { + fmt.Fprintf(out, "This warning will become an error in a future release. To enable filesystem entitlements checks at the moment, set BUILDX_BAKE_ENTITLEMENTS_FS=1 .\n\n") + } + return nil } if term { @@ -173,3 +314,265 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error { return errors.Errorf("additional privileges requested") } + +func isParentOrEqualPath(p, parent string) bool { + if p == parent || parent == "/" { + return true + } + if strings.HasPrefix(p, filepath.Clean(parent+string(filepath.Separator))) { + return true + } + return false +} + +func findMissingPaths(set []string, paths map[string]struct{}) ([]string, error) { + paths, err := evaluateToExistingPaths(paths) + if err != nil { + return nil, err + } + paths, err = dedupPaths(paths) + if err != nil { + return nil, err + } + + set, err = evaluatePaths(set) + if err != nil { + return nil, err + } + + out := make([]string, 0, len(paths)) +loop0: + for p := range paths { + for _, c := range set { + if isParentOrEqualPath(p, c) { + continue loop0 + } + } + out = append(out, p) + } + if len(out) == 0 { + return nil, nil + } + + slices.Sort(out) + + return out, nil +} + +func dedupPaths(in map[string]struct{}) (map[string]struct{}, error) { + arr := make([]string, 0, len(in)) + for p := range in { + arr = append(arr, filepath.Clean(p)) + } + + slices.SortFunc(arr, func(a, b string) int { + return cmp.Compare(len(a), len(b)) + }) + + m := make(map[string]struct{}, len(arr)) +loop0: + for _, p := range arr { + for parent := range m { + if strings.HasPrefix(p, parent+string(filepath.Separator)) { + continue loop0 + } + } + m[p] = struct{}{} + } + return m, nil +} + +func toRelativePaths(in []string, wd string) []string { + out := make([]string, 0, len(in)) + for _, p := range in { + rel, err := filepath.Rel(wd, p) + if err == nil { + // allow up to one level of ".." in the path + if !strings.HasPrefix(rel, ".."+string(filepath.Separator)+"..") { + out = append(out, rel) + continue + } + } + out = append(out, p) + } + return out +} + +func groupSamePaths(in1, in2 []string) ([]string, []string, []string) { + if in1 == nil || in2 == nil { + return in1, in2, nil + } + + slices.Sort(in1) + slices.Sort(in2) + + common := []string{} + i, j := 0, 0 + + for i < len(in1) && j < len(in2) { + switch { + case in1[i] == in2[j]: + common = append(common, in1[i]) + i++ + j++ + case in1[i] < in2[j]: + i++ + default: + j++ + } + } + + in1 = removeCommonPaths(in1, common) + in2 = removeCommonPaths(in2, common) + + return in1, in2, common +} + +func removeCommonPaths(in, common []string) []string { + filtered := make([]string, 0, len(in)) + commonIndex := 0 + for _, path := range in { + if commonIndex < len(common) && path == common[commonIndex] { + commonIndex++ + continue + } + filtered = append(filtered, path) + } + return filtered +} + +func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error) { + m := make(map[string]struct{}, len(in)) + for p := range in { + v, err := evaluateToExistingPath(p) + if err != nil { + return nil, errors.Wrapf(err, "failed to evaluate path %q", p) + } + v, err = osutil.GetLongPathName(v) + if err != nil { + return nil, errors.Wrapf(err, "failed to evaluate path %q", p) + } + m[v] = struct{}{} + } + return m, nil +} + +func evaluateToExistingPath(in string) (string, error) { + in, err := filepath.Abs(in) + if err != nil { + return "", err + } + + volLen := volumeNameLen(in) + pathSeparator := string(os.PathSeparator) + + if volLen < len(in) && os.IsPathSeparator(in[volLen]) { + volLen++ + } + vol := in[:volLen] + dest := vol + linksWalked := 0 + var end int + for start := volLen; start < len(in); start = end { + for start < len(in) && os.IsPathSeparator(in[start]) { + start++ + } + end = start + for end < len(in) && !os.IsPathSeparator(in[end]) { + end++ + } + + if end == start { + break + } else if in[start:end] == "." { + continue + } else if in[start:end] == ".." { + var r int + for r = len(dest) - 1; r >= volLen; r-- { + if os.IsPathSeparator(dest[r]) { + break + } + } + if r < volLen || dest[r+1:] == ".." { + if len(dest) > volLen { + dest += pathSeparator + } + dest += ".." + } else { + dest = dest[:r] + } + continue + } + + if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) { + dest += pathSeparator + } + dest += in[start:end] + + fi, err := os.Lstat(dest) + if err != nil { + // If the component doesn't exist, return the last valid path + if os.IsNotExist(err) { + for r := len(dest) - 1; r >= volLen; r-- { + if os.IsPathSeparator(dest[r]) { + return dest[:r], nil + } + } + return vol, nil + } + return "", err + } + + if fi.Mode()&fs.ModeSymlink == 0 { + if !fi.Mode().IsDir() && end < len(in) { + return "", syscall.ENOTDIR + } + continue + } + + linksWalked++ + if linksWalked > 255 { + return "", errors.New("too many symlinks") + } + + link, err := os.Readlink(dest) + if err != nil { + return "", err + } + + in = link + in[end:] + + v := volumeNameLen(link) + if v > 0 { + if v < len(link) && os.IsPathSeparator(link[v]) { + v++ + } + vol = link[:v] + dest = vol + end = len(vol) + } else if len(link) > 0 && os.IsPathSeparator(link[0]) { + dest = link[:1] + end = 1 + vol = link[:1] + volLen = 1 + } else { + var r int + for r = len(dest) - 1; r >= volLen; r-- { + if os.IsPathSeparator(dest[r]) { + break + } + } + if r < volLen { + dest = vol + } else { + dest = dest[:r] + } + end = 0 + } + } + return filepath.Clean(dest), nil +} + +func volumeNameLen(s string) int { + return len(filepath.VolumeName(s)) +} diff --git a/bake/entitlements_test.go b/bake/entitlements_test.go new file mode 100644 index 00000000000..c3ac586f337 --- /dev/null +++ b/bake/entitlements_test.go @@ -0,0 +1,426 @@ +package bake + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/docker/buildx/build" + "github.com/docker/buildx/controller/pb" + "github.com/docker/buildx/util/osutil" + "github.com/moby/buildkit/client" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/util/entitlements" + "github.com/stretchr/testify/require" +) + +func TestEvaluateToExistingPath(t *testing.T) { + tempDir, err := osutil.GetLongPathName(t.TempDir()) + require.NoError(t, err) + + // Setup temporary directory structure for testing + existingFile := filepath.Join(tempDir, "existing_file") + require.NoError(t, os.WriteFile(existingFile, []byte("test"), 0644)) + + existingDir := filepath.Join(tempDir, "existing_dir") + require.NoError(t, os.Mkdir(existingDir, 0755)) + + symlinkToFile := filepath.Join(tempDir, "symlink_to_file") + require.NoError(t, os.Symlink(existingFile, symlinkToFile)) + + symlinkToDir := filepath.Join(tempDir, "symlink_to_dir") + require.NoError(t, os.Symlink(existingDir, symlinkToDir)) + + nonexistentPath := filepath.Join(tempDir, "nonexistent", "path", "file.txt") + + tests := []struct { + name string + input string + expected string + expectErr bool + }{ + { + name: "Existing file", + input: existingFile, + expected: existingFile, + expectErr: false, + }, + { + name: "Existing directory", + input: existingDir, + expected: existingDir, + expectErr: false, + }, + { + name: "Symlink to file", + input: symlinkToFile, + expected: existingFile, + expectErr: false, + }, + { + name: "Symlink to directory", + input: symlinkToDir, + expected: existingDir, + expectErr: false, + }, + { + name: "Non-existent path", + input: nonexistentPath, + expected: tempDir, + expectErr: false, + }, + { + name: "Non-existent intermediate path", + input: filepath.Join(tempDir, "nonexistent", "file.txt"), + expected: tempDir, + expectErr: false, + }, + { + name: "Root path", + input: "/", + expected: func() string { + root, _ := filepath.Abs("/") + return root + }(), + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := evaluateToExistingPath(tt.input) + + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +} + +func TestDedupePaths(t *testing.T) { + wd := osutil.GetWd() + tcases := []struct { + in map[string]struct{} + out map[string]struct{} + }{ + { + in: map[string]struct{}{ + "/a/b/c": {}, + "/a/b/d": {}, + "/a/b/e": {}, + }, + out: map[string]struct{}{ + "/a/b/c": {}, + "/a/b/d": {}, + "/a/b/e": {}, + }, + }, + { + in: map[string]struct{}{ + "/a/b/c": {}, + "/a/b/c/d": {}, + "/a/b/c/d/e": {}, + "/a/b/../b/c": {}, + }, + out: map[string]struct{}{ + "/a/b/c": {}, + }, + }, + { + in: map[string]struct{}{ + filepath.Join(wd, "a/b/c"): {}, + filepath.Join(wd, "../aa"): {}, + filepath.Join(wd, "a/b"): {}, + filepath.Join(wd, "a/b/d"): {}, + filepath.Join(wd, "../aa/b"): {}, + filepath.Join(wd, "../../bb"): {}, + }, + out: map[string]struct{}{ + "a/b": {}, + "../aa": {}, + filepath.Join(wd, "../../bb"): {}, + }, + }, + } + + for i, tc := range tcases { + t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) { + out, err := dedupPaths(tc.in) + if err != nil { + require.NoError(t, err) + } + // convert to relative paths as that is shown to user + arr := make([]string, 0, len(out)) + for k := range out { + arr = append(arr, k) + } + require.NoError(t, err) + arr = toRelativePaths(arr, wd) + m := make(map[string]struct{}) + for _, v := range arr { + m[filepath.ToSlash(v)] = struct{}{} + } + o := make(map[string]struct{}, len(tc.out)) + for k := range tc.out { + o[filepath.ToSlash(k)] = struct{}{} + } + require.Equal(t, o, m) + }) + } +} + +func TestValidateEntitlements(t *testing.T) { + dir1, err := osutil.GetLongPathName(t.TempDir()) + require.NoError(t, err) + dir2, err := osutil.GetLongPathName(t.TempDir()) + require.NoError(t, err) + + escapeLink := filepath.Join(dir1, "escape_link") + require.NoError(t, os.Symlink("../../aa", escapeLink)) + + wd := osutil.GetWd() + + tcases := []struct { + name string + conf EntitlementConf + opt build.Options + expected EntitlementConf + }{ + { + name: "No entitlements", + opt: build.Options{ + Inputs: build.Inputs{ + ContextState: &llb.State{}, + }, + }, + }, + { + name: "NetworkHostMissing", + opt: build.Options{ + Allow: []entitlements.Entitlement{ + entitlements.EntitlementNetworkHost, + }, + }, + expected: EntitlementConf{ + NetworkHost: true, + FSRead: []string{wd}, + }, + }, + { + name: "NetworkHostSet", + conf: EntitlementConf{ + NetworkHost: true, + }, + opt: build.Options{ + Allow: []entitlements.Entitlement{ + entitlements.EntitlementNetworkHost, + }, + }, + expected: EntitlementConf{ + FSRead: []string{wd}, + }, + }, + { + name: "SecurityAndNetworkHostMissing", + opt: build.Options{ + Allow: []entitlements.Entitlement{ + entitlements.EntitlementNetworkHost, + entitlements.EntitlementSecurityInsecure, + }, + }, + expected: EntitlementConf{ + NetworkHost: true, + SecurityInsecure: true, + FSRead: []string{wd}, + }, + }, + { + name: "SecurityMissingAndNetworkHostSet", + conf: EntitlementConf{ + NetworkHost: true, + }, + opt: build.Options{ + Allow: []entitlements.Entitlement{ + entitlements.EntitlementNetworkHost, + entitlements.EntitlementSecurityInsecure, + }, + }, + expected: EntitlementConf{ + SecurityInsecure: true, + FSRead: []string{wd}, + }, + }, + { + name: "SSHMissing", + opt: build.Options{ + SSHSpecs: []*pb.SSH{ + { + ID: "test", + }, + }, + }, + expected: EntitlementConf{ + SSH: true, + FSRead: []string{wd}, + }, + }, + { + name: "ExportLocal", + opt: build.Options{ + Exports: []client.ExportEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": dir1, + }, + }, + { + Type: "local", + Attrs: map[string]string{ + "dest": filepath.Join(dir1, "subdir"), + }, + }, + { + Type: "local", + Attrs: map[string]string{ + "dest": dir2, + }, + }, + }, + }, + expected: EntitlementConf{ + FSWrite: func() []string { + exp := []string{dir1, dir2} + slices.Sort(exp) + return exp + }(), + FSRead: []string{wd}, + }, + }, + { + name: "SecretFromSubFile", + opt: build.Options{ + SecretSpecs: []*pb.Secret{ + { + FilePath: filepath.Join(dir1, "subfile"), + }, + }, + }, + conf: EntitlementConf{ + FSRead: []string{wd, dir1}, + }, + }, + { + name: "SecretFromEscapeLink", + opt: build.Options{ + SecretSpecs: []*pb.Secret{ + { + FilePath: escapeLink, + }, + }, + }, + conf: EntitlementConf{ + FSRead: []string{wd, dir1}, + }, + expected: EntitlementConf{ + FSRead: []string{filepath.Join(dir1, "../..")}, + }, + }, + { + name: "SecretFromEscapeLinkAllowRoot", + opt: build.Options{ + SecretSpecs: []*pb.Secret{ + { + FilePath: escapeLink, + }, + }, + }, + conf: EntitlementConf{ + FSRead: []string{"/"}, + }, + expected: EntitlementConf{}, + }, + } + + for _, tc := range tcases { + t.Run(tc.name, func(t *testing.T) { + expected, err := tc.conf.Validate(map[string]build.Options{"test": tc.opt}) + require.NoError(t, err) + require.Equal(t, tc.expected, expected) + }) + } +} + +func TestGroupSamePaths(t *testing.T) { + tests := []struct { + name string + in1 []string + in2 []string + expected1 []string + expected2 []string + expectedC []string + }{ + { + name: "All common paths", + in1: []string{"/path/a", "/path/b", "/path/c"}, + in2: []string{"/path/a", "/path/b", "/path/c"}, + expected1: []string{}, + expected2: []string{}, + expectedC: []string{"/path/a", "/path/b", "/path/c"}, + }, + { + name: "No common paths", + in1: []string{"/path/a", "/path/b"}, + in2: []string{"/path/c", "/path/d"}, + expected1: []string{"/path/a", "/path/b"}, + expected2: []string{"/path/c", "/path/d"}, + expectedC: []string{}, + }, + { + name: "Some common paths", + in1: []string{"/path/a", "/path/b", "/path/c"}, + in2: []string{"/path/b", "/path/c", "/path/d"}, + expected1: []string{"/path/a"}, + expected2: []string{"/path/d"}, + expectedC: []string{"/path/b", "/path/c"}, + }, + { + name: "Empty inputs", + in1: []string{}, + in2: []string{}, + expected1: []string{}, + expected2: []string{}, + expectedC: []string{}, + }, + { + name: "One empty input", + in1: []string{"/path/a", "/path/b"}, + in2: []string{}, + expected1: []string{"/path/a", "/path/b"}, + expected2: []string{}, + expectedC: []string{}, + }, + { + name: "Unsorted inputs with common paths", + in1: []string{"/path/c", "/path/a", "/path/b"}, + in2: []string{"/path/b", "/path/c", "/path/a"}, + expected1: []string{}, + expected2: []string{}, + expectedC: []string{"/path/a", "/path/b", "/path/c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out1, out2, common := groupSamePaths(tt.in1, tt.in2) + require.Equal(t, tt.expected1, out1, "in1 should match expected1") + require.Equal(t, tt.expected2, out2, "in2 should match expected2") + require.Equal(t, tt.expectedC, common, "common should match expectedC") + }) + } +} diff --git a/bake/entitlements_unix.go b/bake/entitlements_unix.go new file mode 100644 index 00000000000..660bc6fe692 --- /dev/null +++ b/bake/entitlements_unix.go @@ -0,0 +1,26 @@ +//go:build !windows +// +build !windows + +package bake + +import ( + "path/filepath" + + "github.com/pkg/errors" +) + +func evaluatePaths(in []string) ([]string, error) { + out := make([]string, 0, len(in)) + for _, p := range in { + v, err := filepath.Abs(p) + if err != nil { + return nil, err + } + v, err = filepath.EvalSymlinks(v) + if err != nil { + return nil, errors.Wrapf(err, "failed to evaluate path %q", p) + } + out = append(out, v) + } + return out, nil +} diff --git a/bake/entitlements_windows.go b/bake/entitlements_windows.go new file mode 100644 index 00000000000..c1cda8c0738 --- /dev/null +++ b/bake/entitlements_windows.go @@ -0,0 +1,39 @@ +package bake + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +func evaluatePaths(in []string) ([]string, error) { + out := make([]string, 0, len(in)) + for _, p := range in { + if p == "/" { + out = append(out, getAllVolumes()...) + continue + } + v, err := filepath.Abs(p) + if err != nil { + return nil, err + } + v, err = filepath.EvalSymlinks(v) + if err != nil { + return nil, errors.Wrapf(err, "failed to evaluate path %q", p) + } + out = append(out, v) + } + return out, nil +} + +func getAllVolumes() []string { + var volumes []string + for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { + p := string(drive) + ":" + string(filepath.Separator) + if _, err := os.Stat(p); !os.IsNotExist(err) { + volumes = append(volumes, p) + } + } + return volumes +} diff --git a/build/build.go b/build/build.go index 17cd7028e69..3a2e7663dd7 100644 --- a/build/build.go +++ b/build/build.go @@ -18,6 +18,7 @@ import ( "github.com/containerd/containerd/images" "github.com/distribution/reference" "github.com/docker/buildx/builder" + controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/driver" "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/desktop" @@ -76,6 +77,8 @@ type Options struct { NoCacheFilter []string Platforms []specs.Platform Pull bool + SecretSpecs []*controllerapi.Secret + SSHSpecs []*controllerapi.SSH ShmSize opts.MemBytes Tags []string Target string diff --git a/commands/bake.go b/commands/bake.go index 4737c138c20..ad94c23f31c 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -107,6 +107,13 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err != nil { return err } + wd, err := os.Getwd() + if err != nil { + return errors.Wrapf(err, "failed to get current working directory") + } + // filesystem access under the current working directory is allowed by default + ent.FSRead = append(ent.FSRead, wd) + ent.FSWrite = append(ent.FSWrite, wd) ctx2, cancel := context.WithCancelCause(context.TODO()) defer cancel(errors.WithStack(context.Canceled)) @@ -250,7 +257,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err != nil { return err } - if err := exp.Prompt(ctx, &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil { + if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil { return err } if printer.IsDone() { diff --git a/tests/bake.go b/tests/bake.go index a6bad6ebb6c..8f92cb8e668 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -508,7 +508,8 @@ EOT withArgs(addr, "--set", "*.output=type=local,dest="+dirDest), ) require.Error(t, err, out) - require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS") + require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities") + require.Contains(t, out, "Read access to path ../") out, err = bakeCmd( sb, @@ -555,7 +556,8 @@ EOT withArgs(addr, "--set", "*.output=type=local,dest="+dirDest), ) require.Error(t, err, out) - require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS") + require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities") + require.Contains(t, out, "Read access to path ..") out, err = bakeCmd( sb,