Skip to content

Commit

Permalink
repack: do not include Volume paths in new layers
Browse files Browse the repository at this point in the history
This is (unfortunately) not mandated in the specification[1,2], but in
order to avoid accidentally spilling private information into published
layers (which is one use of Volumes) we must ignore all layers included
in Config.Volumes.

In future we should also add some flags or alernative ways of masking
paths.

[1]: opencontainers/image-spec#496
[2]: opencontainers/image-spec#504

Signed-off-by: Aleksa Sarai <asarai@suse.de>
  • Loading branch information
cyphar committed Jun 3, 2017
1 parent 2827f27 commit 2fc647f
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 0 deletions.
12 changes: 12 additions & 0 deletions cmd/umoci/repack.go
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/openSUSE/umoci/oci/cas"
igen "github.com/openSUSE/umoci/oci/config/generate"
"github.com/openSUSE/umoci/oci/layer"
"github.com/openSUSE/umoci/pkg/mtreefilter"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/urfave/cli"
Expand Down Expand Up @@ -155,6 +156,17 @@ func repack(ctx *cli.Context) error {
"ndiff": len(diffs),
}).Debugf("umoci: checked mtree spec")

// We need to mask config.Volumes.
config, err := mutator.Config(context.Background())
if err != nil {
return errors.Wrap(err, "get config")
}
var volumes []string
for v := range config.Volumes {
volumes = append(volumes, v)
}
diffs = mtreefilter.FilterDeltas(diffs, mtreefilter.MaskFilter(volumes))

reader, err := layer.GenerateLayer(fullRootfsPath, diffs, &meta.MapOptions)
if err != nil {
return errors.Wrap(err, "generate diff layer")
Expand Down
81 changes: 81 additions & 0 deletions pkg/mtreefilter/mask.go
@@ -0,0 +1,81 @@
/*
* umoci: Umoci Modifies Open Containers' Images
* Copyright (C) 2017 SUSE LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package mtreefilter

import (
"path/filepath"

"github.com/apex/log"
"github.com/vbatts/go-mtree"
)

// FilterFunc is a function used when filtering deltas with FilterDeltas.
type FilterFunc func(path string) bool

// isParent returns whether the path a is lexically an ancestor of the path b.
func isParent(a, b string) bool {
a = filepath.Clean(a)
b = filepath.Clean(b)

for a != b {
if b == filepath.Dir(b) {
break
}
b = filepath.Dir(b)
}
return a == b
}

// MaskFilter is a factory for FilterFuncs that will mask all InodeDelta paths
// that are lexical children of any path in the mask slice. All paths are
// considered to be relative to '/'.
func MaskFilter(masks []string) FilterFunc {
return func(path string) bool {
// Convert path to relative-to-root.
path = filepath.Clean(path)
path = filepath.Join("/", path)

// Check that no masks are matched.
for _, mask := range masks {
// Mask also needs to be relative-to-root.
mask = filepath.Clean(mask)
mask = filepath.Join("/", mask)

// Is it a parent?
if isParent(mask, path) {
log.Debugf("maskfilter: ignoring path %q matched by mask %q", path, mask)
return false
}
}

return true
}
}

// FilterDeltas is a helper function to easily filter []mtree.InodeDelta with a
// filter function. Only entries which have `filter(delta.Path()) == true` will
// be included in the returned slice.
func FilterDeltas(deltas []mtree.InodeDelta, filter FilterFunc) []mtree.InodeDelta {
var filtered []mtree.InodeDelta
for _, delta := range deltas {
if filter(delta.Path()) {
filtered = append(filtered, delta)
}
}
return filtered
}
144 changes: 144 additions & 0 deletions pkg/mtreefilter/mask_test.go
@@ -0,0 +1,144 @@
/*
* umoci: Umoci Modifies Open Containers' Images
* Copyright (C) 2017 SUSE LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package mtreefilter

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/vbatts/go-mtree"
)

func TestIsParent(t *testing.T) {
for _, test := range []struct {
parent, path string
expected bool
}{
{"/", "/a", true},
{"/", "/a/b/c", true},
{"/", "/", true},
{"/a path/", "/a path", true},
{"/a nother path", "/a nother path/test", true},
{"/a nother path", "/a nother path/test/1 2/ 33 /", true},
{"/path1", "/path2", false},
{"/pathA", "/PATHA", false},
{"/pathC", "/path", false},
{"/path9", "/", false},
// Make sure it's not the same as filepath.HasPrefix.
{"/a/b/c", "/a/b/c/d", true},
{"/a/b/c", "/a/b/cd", false},
{"/a/b/c", "/a/bcd", false},
{"/a/bc", "/a/bcd", false},
{"/abc", "/abcd", false},
} {
got := isParent(test.parent, test.path)
if got != test.expected {
t.Errorf("isParent(%q, %q) got %v expected %v", test.parent, test.path, got, test.expected)
}
}
}

func TestMaskDeltas(t *testing.T) {
dir, err := ioutil.TempDir("", "TestMaskDeltas-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)

var mtreeKeywords []mtree.Keyword = append(mtree.DefaultKeywords, "sha256digest")

// Create some files.
if err != ioutil.WriteFile(filepath.Join(dir, "file1"), []byte("contents"), 0644) {
t.Fatal(err)
}
if err != ioutil.WriteFile(filepath.Join(dir, "file2"), []byte("another content"), 0644) {
t.Fatal(err)
}
if err != os.MkdirAll(filepath.Join(dir, "dir", "child"), 0755) {
t.Fatal(err)
}
if err != os.MkdirAll(filepath.Join(dir, "dir", "child2"), 0755) {
t.Fatal(err)
}
if err != ioutil.WriteFile(filepath.Join(dir, "dir", "file 3"), []byte("more content"), 0644) {
t.Fatal(err)
}
if err != ioutil.WriteFile(filepath.Join(dir, "dir", "child2", "4 files"), []byte("very content"), 0644) {
t.Fatal(err)
}

// Generate a diff.
originalDh, err := mtree.Walk(dir, nil, mtreeKeywords, nil)
if err != nil {
t.Fatal(err)
}

// Modify the root.
if err := os.RemoveAll(filepath.Join(dir, "file2")); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(filepath.Join(dir, "dir", "new"), []byte("more content"), 0755); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(filepath.Join(dir, "file1"), []byte("different contents"), 0666); err != nil {
t.Fatal(err)
}

// Generate the set of diffs.
newDh, err := mtree.Walk(dir, nil, mtreeKeywords, nil)
if err != nil {
t.Fatal(err)
}
diff, err := mtree.Compare(originalDh, newDh, mtreeKeywords)
if err != nil {
t.Fatal(err)
}

for _, test := range []struct {
paths []string
}{
{nil},
{[]string{"/"}},
{[]string{"dir"}},
{[]string{filepath.Join("dir", "child2")}},
{[]string{"file2"}},
{[]string{"/", "file2"}},
{[]string{"file2", filepath.Join("dir", "child2")}},
} {
newDiff := FilterDeltas(diff, MaskFilter(test.paths))
for _, delta := range newDiff {
if len(test.paths) == 0 {
if len(newDiff) != len(diff) {
t.Errorf("expected diff={} to give %d got %d", len(diff), len(newDiff))
}
} else {
found := false
for _, path := range test.paths {
if !isParent(path, delta.Path()) {
found = true
}
}
if !found {
t.Errorf("expected one of %v to not be a parent of %q", test.paths, delta.Path())
}
}
}
}
}

0 comments on commit 2fc647f

Please sign in to comment.