Compare commits

...

8 Commits

Author SHA1 Message Date
CrazyMax
48d6a3927a Merge pull request #2867 from crazy-max/v0.19_backport_entitlement-path-fix
[v0.19 backport] bake: change evaluation of entitlement paths
2024-12-17 12:25:19 +01:00
Tonis Tiigi
2bd4aefb9b add additional test coverage for FS entitlement paths
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2024-12-17 10:16:56 +01:00
Tonis Tiigi
f1b3cf74eb bake: change evaluation of entitlement paths
Currently, to compare the local path used by bake against the paths allowed
by entitlements, symlinks were evaluated for path normalization so that the
local path used by build was allowed to not exist while the path allowed by
entitlement needed to exist. If the path used by the build did not exist,
then the deepest existing parent path was used instead. This was concistent
with entitlement rules as that parent path would be the actual path access
is needed.

This raised an issue with `--set` if one provides a non-existing path as
an argument, as these paths are supposed to be allowed automatically. With
the above restrictions set to allowed paths, this meant the build would fail
as it can't grant entitlement to the non-existing paths.

This changes the evaluation logic for allowing paths so that they do not
need to exist. If such a case appears, then the path is evaluated to the
last component that exists, and then the rest of the path is appended as is.

This means that for example, if `output = /tmp/out/foo/` is set in HCL
and `/tmp` is the last component that exists then invoking build with
`--allow fs.write=/tmp/out/foo` will not fail with stat error anymore
but will fail in entitlements validation as build would also need to
write `/tmp/out` that is not inside the allowed `/tmp/out/foo` path. The
same would apply to `--set` as well so that if it points to
a non-existing path, then an additional `--allow` rule is needed
providing access to writing to the last existing component of that path.
This may or may not be unexpected.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2024-12-17 10:15:21 +01:00
Tõnis Tiigi
62d486d5a5 Merge pull request #2861 from tonistiigi/v0.19-set-empty-eof
[v0.19] bake: remove empty values set by --set
2024-12-16 10:16:22 -08:00
CrazyMax
afc9cebb48 bake: test empty override
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2024-12-16 18:35:14 +01:00
Tonis Tiigi
9a9dd4e87e [v0.19] bake: remove empty values set by --set
These parser functions are called for `--set` to
resolve entitlement paths that would be automatically added
and will fail for empty value.

The empty values would already be ignored but in v0.19 is
done after merging the `--set` values and then calling
parse again.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2024-12-16 09:12:36 -08:00
CrazyMax
85acd30ed7 Merge pull request #2862 from crazy-max/v0.19_pick-xx-update
[v0.19 backport] update xx to v1.6.1
2024-12-16 13:20:09 +01:00
Tonis Tiigi
90393e68a4 update xx to v1.6.1
Fixes compatibility issues with Alpine 3.21

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2024-12-16 10:39:49 +01:00
9 changed files with 359 additions and 15 deletions

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.23
ARG XX_VERSION=1.5.0
ARG XX_VERSION=1.6.1
# for testing
ARG DOCKER_VERSION=27.4.0-rc.2

View File

@@ -2028,3 +2028,20 @@ target "app" {
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, &EntitlementConf{})
require.NoError(t, err)
}
// https://github.com/docker/buildx/issues/2858
func TestOverrideEmpty(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(`
target "app" {
output = ["./bin"]
}
`),
}
ctx := context.TODO()
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, []string{"app.output="}, nil, &EntitlementConf{})
require.NoError(t, err)
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/docker/buildx/util/osutil"
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type EntitlementKey string
@@ -444,12 +445,20 @@ func evaluatePaths(in []string) ([]string, bool, error) {
}
v, err := filepath.Abs(p)
if err != nil {
return nil, false, errors.Wrapf(err, "failed to evaluate path %q", p)
logrus.Warnf("failed to evaluate entitlement path %q: %v", p, err)
continue
}
v, err = filepath.EvalSymlinks(v)
v, rest, err := evaluateToExistingPath(v)
if err != nil {
return nil, false, errors.Wrapf(err, "failed to evaluate path %q", p)
}
v, err = osutil.GetLongPathName(v)
if err != nil {
return nil, false, errors.Wrapf(err, "failed to evaluate path %q", p)
}
if rest != "" {
v = filepath.Join(v, rest)
}
out = append(out, v)
}
return out, allowAny, nil
@@ -458,7 +467,7 @@ func evaluatePaths(in []string) ([]string, bool, error) {
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)
v, _, err := evaluateToExistingPath(p)
if err != nil {
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
}
@@ -471,10 +480,10 @@ func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error
return m, nil
}
func evaluateToExistingPath(in string) (string, error) {
func evaluateToExistingPath(in string) (string, string, error) {
in, err := filepath.Abs(in)
if err != nil {
return "", err
return "", "", err
}
volLen := volumeNameLen(in)
@@ -529,29 +538,29 @@ func evaluateToExistingPath(in string) (string, error) {
if os.IsNotExist(err) {
for r := len(dest) - 1; r >= volLen; r-- {
if os.IsPathSeparator(dest[r]) {
return dest[:r], nil
return dest[:r], in[start:], nil
}
}
return vol, nil
return vol, in[start:], nil
}
return "", err
return "", "", err
}
if fi.Mode()&fs.ModeSymlink == 0 {
if !fi.Mode().IsDir() && end < len(in) {
return "", syscall.ENOTDIR
return "", "", syscall.ENOTDIR
}
continue
}
linksWalked++
if linksWalked > 255 {
return "", errors.New("too many symlinks")
return "", "", errors.New("too many symlinks")
}
link, err := os.Readlink(dest)
if err != nil {
return "", err
return "", "", err
}
in = link + in[end:]
@@ -584,7 +593,7 @@ func evaluateToExistingPath(in string) (string, error) {
end = 0
}
}
return filepath.Clean(dest), nil
return filepath.Clean(dest), "", nil
}
func volumeNameLen(s string) int {

View File

@@ -89,7 +89,7 @@ func TestEvaluateToExistingPath(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := evaluateToExistingPath(tt.input)
result, _, err := evaluateToExistingPath(tt.input)
if tt.expectErr {
require.Error(t, err)
@@ -341,7 +341,7 @@ func TestValidateEntitlements(t *testing.T) {
return nil
}
// if not, then escapeLink is not allowed
exp, err := evaluateToExistingPath(escapeLink)
exp, _, err := evaluateToExistingPath(escapeLink)
require.NoError(t, err)
exp, err = filepath.EvalSymlinks(exp)
require.NoError(t, err)
@@ -363,6 +363,48 @@ func TestValidateEntitlements(t *testing.T) {
},
expected: EntitlementConf{},
},
{
name: "NonExistingAllowedPathSubpath",
opt: build.Options{
ExportsLocalPathsTemporary: []string{
dir1,
},
},
conf: EntitlementConf{
FSRead: []string{wd},
FSWrite: []string{filepath.Join(dir1, "not/exists")},
},
expected: EntitlementConf{
FSWrite: []string{expDir1}, // dir1 is still needed as only subpath was allowed
},
},
{
name: "NonExistingAllowedPathMatches",
opt: build.Options{
ExportsLocalPathsTemporary: []string{
filepath.Join(dir1, "not/exists"),
},
},
conf: EntitlementConf{
FSRead: []string{wd},
FSWrite: []string{filepath.Join(dir1, "not/exists")},
},
expected: EntitlementConf{
FSWrite: []string{expDir1}, // dir1 is still needed as build also needs to write not/exists directory
},
},
{
name: "NonExistingBuildPath",
opt: build.Options{
ExportsLocalPathsTemporary: []string{
filepath.Join(dir1, "not/exists"),
},
},
conf: EntitlementConf{
FSRead: []string{wd},
FSWrite: []string{dir1},
},
},
}
for _, tc := range tcases {

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
@@ -46,6 +47,14 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
testBakeRemoteDockerfileCwd,
testBakeRemoteLocalContextRemoteDockerfile,
testBakeEmpty,
testBakeSetNonExistingSubdirNoParallel,
testBakeSetNonExistingOutsideNoParallel,
testBakeSetExistingOutsideNoParallel,
testBakeDefinitionNotExistingSubdirNoParallel,
testBakeDefinitionNotExistingOutsideNoParallel,
testBakeDefinitionExistingOutsideNoParallel,
testBakeDefinitionSymlinkOutsideNoParallel,
testBakeDefinitionSymlinkOutsideGrantedNoParallel,
testBakeShmSize,
testBakeUlimits,
testBakeMetadataProvenance,
@@ -705,6 +714,261 @@ target "default" {
require.Contains(t, string(dt), `size=131072k`)
}
func testBakeSetNonExistingSubdirNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
bakefile := []byte(`
target "default" {
}
`)
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain", "--set", "*.output=type=local,dest="+filepath.Join(dir, "not/exists")))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), `#1 [internal] load local bake definitions`)
require.Contains(t, string(out), `#1 reading docker-bake.hcl`)
require.FileExists(t, filepath.Join(dir, "not/exists/foo"))
})
}
}
func testBakeSetNonExistingOutsideNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
bakefile := []byte(`
target "default" {
}
`)
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
destDir := t.TempDir()
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain", "--set", "*.output=type=local,dest="+filepath.Join(destDir, "not/exists")))
out, err := cmd.CombinedOutput()
if ent {
require.Error(t, err, string(out))
require.Contains(t, string(out), "ERROR: additional privileges requested")
} else {
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(destDir, "not/exists/foo"))
}
})
}
}
func testBakeSetExistingOutsideNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
bakefile := []byte(`
target "default" {
}
`)
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
destDir := t.TempDir()
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain", "--set", "*.output=type=local,dest="+destDir))
out, err := cmd.CombinedOutput()
// existing directory via --set is always allowed
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(destDir, "foo"))
})
}
}
func testBakeDefinitionNotExistingSubdirNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
bakefile := []byte(`
target "default" {
output = ["type=local,dest=not/exists"]
}
`)
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain"))
out, err := cmd.CombinedOutput()
// subdirs of working directory are always allowed
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(dir, "not/exists/foo"))
})
}
}
func testBakeDefinitionNotExistingOutsideNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
destDir := t.TempDir()
bakefile := []byte(fmt.Sprintf(`
target "default" {
output = ["type=local,dest=%s/not/exists"]
}
`, destDir))
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain"))
out, err := cmd.CombinedOutput()
if ent {
require.Error(t, err, string(out))
require.Contains(t, string(out), "ERROR: additional privileges requested")
} else {
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(destDir, "not/exists/foo"))
}
})
}
}
func testBakeDefinitionExistingOutsideNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
destDir := t.TempDir()
bakefile := []byte(fmt.Sprintf(`
target "default" {
output = ["type=local,dest=%s"]
}
`, destDir))
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain"))
out, err := cmd.CombinedOutput()
if ent {
require.Error(t, err, string(out))
require.Contains(t, string(out), "ERROR: additional privileges requested")
} else {
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(destDir, "foo"))
}
})
}
}
func testBakeDefinitionSymlinkOutsideNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
destDir := t.TempDir()
bakefile := []byte(`
target "default" {
output = ["type=local,dest=out"]
}
`)
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
fstest.Symlink(destDir, "out"),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain"))
out, err := cmd.CombinedOutput()
if ent {
require.Error(t, err, string(out))
require.Contains(t, string(out), "ERROR: additional privileges requested")
} else {
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(destDir, "foo"))
}
})
}
}
func testBakeDefinitionSymlinkOutsideGrantedNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", strconv.FormatBool(ent))
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
destDir := t.TempDir()
bakefile := []byte(`
target "default" {
output = ["type=local,dest=out"]
}
`)
dir := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
fstest.Symlink(destDir, "out"),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--progress=plain", "--allow", "fs.write="+destDir))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(destDir, "foo"))
})
}
}
func testBakeUlimits(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox AS build

View File

@@ -14,6 +14,9 @@ import (
func ParseCacheEntry(in []string) ([]*controllerapi.CacheOptionsEntry, error) {
outs := make([]*controllerapi.CacheOptionsEntry, 0, len(in))
for _, in := range in {
if in == "" {
continue
}
fields, err := csvvalue.Fields(in, nil)
if err != nil {
return nil, err

View File

@@ -19,6 +19,9 @@ func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) {
return nil, nil
}
for _, s := range inp {
if s == "" {
continue
}
fields, err := csvvalue.Fields(s, nil)
if err != nil {
return nil, err

View File

@@ -11,6 +11,9 @@ import (
func ParseSecretSpecs(sl []string) ([]*controllerapi.Secret, error) {
fs := make([]*controllerapi.Secret, 0, len(sl))
for _, v := range sl {
if v == "" {
continue
}
s, err := parseSecret(v)
if err != nil {
return nil, err

View File

@@ -14,6 +14,9 @@ func ParseSSHSpecs(sl []string) ([]*controllerapi.SSH, error) {
}
for _, s := range sl {
if s == "" {
continue
}
parts := strings.SplitN(s, "=", 2)
out := controllerapi.SSH{
ID: parts[0],