Files
rkt/pkg/tar/tar_test.go
Simone Gotti 2c66843cc1 aci: extract tar in chroot
this introduces tar extraction inside a chroot. This is done spawning a
new process. Instead of creating a new executable (difficult to find,
package etc...) it adds the concept of a multicall process (like busybox).

Now a new multicall command "extracttar" is added.
tar.ExtractTar executes this new command passing a pipe to send the
filesMap and the tar contents to stdin.

The incomplete "insecure link" checks are removed from the the tar
functions.  Additionally these functions are now not exported and the
extraction directory is always the root directory.
2015-05-26 23:40:36 +02:00

694 lines
15 KiB
Go

// Copyright 2014 The rkt Authors
//
// 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 tar
import (
"archive/tar"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/coreos/rkt/pkg/multicall"
"github.com/coreos/rkt/pkg/sys"
)
func init() {
multicall.MaybeExec()
}
type testTarEntry struct {
header *tar.Header
contents string
}
func newTestTar(entries []*testTarEntry) (string, error) {
t, err := ioutil.TempFile("", "test-tar")
if err != nil {
return "", err
}
defer t.Close()
tw := tar.NewWriter(t)
for _, entry := range entries {
// Add default mode
if entry.header.Mode == 0 {
if entry.header.Typeflag == tar.TypeDir {
entry.header.Mode = 0755
} else {
entry.header.Mode = 0644
}
}
// Add calling user uid and gid or tests will fail
entry.header.Uid = os.Getuid()
entry.header.Gid = os.Getgid()
if err := tw.WriteHeader(entry.header); err != nil {
return "", err
}
if _, err := io.WriteString(tw, entry.contents); err != nil {
return "", err
}
}
if err := tw.Close(); err != nil {
return "", err
}
return t.Name(), nil
}
type fileInfo struct {
path string
typeflag byte
size int64
contents string
mode os.FileMode
}
func fileInfoSliceToMap(slice []*fileInfo) map[string]*fileInfo {
fim := make(map[string]*fileInfo, len(slice))
for _, fi := range slice {
fim[fi.path] = fi
}
return fim
}
func checkExpectedFiles(dir string, expectedFiles map[string]*fileInfo) error {
files := make(map[string]*fileInfo)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
fm := info.Mode()
if path == dir {
return nil
}
relpath, err := filepath.Rel(dir, path)
if err != nil {
return err
}
switch {
case fm.IsRegular():
files[relpath] = &fileInfo{path: relpath, typeflag: tar.TypeReg, size: info.Size(), mode: info.Mode().Perm()}
case info.IsDir():
files[relpath] = &fileInfo{path: relpath, typeflag: tar.TypeDir, mode: info.Mode().Perm()}
case fm&os.ModeSymlink != 0:
files[relpath] = &fileInfo{path: relpath, typeflag: tar.TypeSymlink, mode: info.Mode()}
default:
return fmt.Errorf("file mode not handled: %v", fm)
}
return nil
})
if err != nil {
return err
}
// Set defaults for not specified expected file mode
for _, ef := range expectedFiles {
if ef.mode == 0 {
if ef.typeflag == tar.TypeDir {
ef.mode = 0755
} else {
ef.mode = 0644
}
}
}
for _, ef := range expectedFiles {
_, ok := files[ef.path]
if !ok {
return fmt.Errorf("Expected file %q not in files", ef.path)
}
}
for _, file := range files {
ef, ok := expectedFiles[file.path]
if !ok {
return fmt.Errorf("file %q not in expectedFiles", file.path)
}
if ef.typeflag != file.typeflag {
return fmt.Errorf("file %q: file type differs: wanted: %d, got: %d", file.path, ef.typeflag, file.typeflag)
}
if ef.typeflag == tar.TypeReg {
if ef.size != file.size {
return fmt.Errorf("file %q: size differs: wanted %d, wanted: %d", file.path, ef.size, file.size)
}
if ef.contents != "" {
buf, err := ioutil.ReadFile(filepath.Join(dir, file.path))
if err != nil {
return fmt.Errorf("unexpected error: %v", err)
}
if string(buf) != ef.contents {
return fmt.Errorf("unexpected contents, wanted: %s, got: %s", ef.contents, buf)
}
}
}
// Check modes but ignore symlinks
if ef.mode != file.mode && ef.typeflag != tar.TypeSymlink {
return fmt.Errorf("file %q: mode differs: wanted %#o, got: %#o", file.path, ef.mode, file.mode)
}
}
return nil
}
func TestExtractTarFolders(t *testing.T) {
if !sys.HasChrootCapability() {
t.Skipf("chroot capability not available. Disabling test.")
}
entries := []*testTarEntry{
{
contents: "foo",
header: &tar.Header{
Name: "deep/folder/foo.txt",
Size: 3,
},
},
{
header: &tar.Header{
Name: "deep/folder/",
Typeflag: tar.TypeDir,
Mode: int64(0747),
},
},
{
contents: "bar",
header: &tar.Header{
Name: "deep/folder/bar.txt",
Size: 3,
},
},
{
header: &tar.Header{
Name: "deep/folder2/symlink.txt",
Typeflag: tar.TypeSymlink,
Linkname: "deep/folder/foo.txt",
},
},
{
header: &tar.Header{
Name: "deep/folder2/",
Typeflag: tar.TypeDir,
Mode: int64(0747),
},
},
{
contents: "bar",
header: &tar.Header{
Name: "deep/folder2/bar.txt",
Size: 3,
},
},
{
header: &tar.Header{
Name: "deep/deep/folder",
Typeflag: tar.TypeDir,
Mode: int64(0755),
},
},
{
header: &tar.Header{
Name: "deep/deep/",
Typeflag: tar.TypeDir,
Mode: int64(0747),
},
},
}
testTarPath, err := newTestTar(entries)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer os.Remove(testTarPath)
containerTar, err := os.Open(testTarPath)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer containerTar.Close()
tmpdir, err := ioutil.TempDir("", "rkt-temp-dir")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
os.RemoveAll(tmpdir)
err = os.MkdirAll(tmpdir, 0755)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer os.RemoveAll(tmpdir)
err = ExtractTar(containerTar, tmpdir, false, nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
matches, err := filepath.Glob(filepath.Join(tmpdir, "deep/folder/*.txt"))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(matches) != 2 {
t.Errorf("unexpected number of files found: %d, wanted 2", len(matches))
}
matches, err = filepath.Glob(filepath.Join(tmpdir, "deep/folder2/*.txt"))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(matches) != 2 {
t.Errorf("unexpected number of files found: %d, wanted 2", len(matches))
}
dirInfo, err := os.Lstat(filepath.Join(tmpdir, "deep/folder"))
if err != nil {
t.Errorf("unexpected error: %v", err)
} else if dirInfo.Mode().Perm() != os.FileMode(0747) {
t.Errorf("unexpected dir mode: %s", dirInfo.Mode())
}
dirInfo, err = os.Lstat(filepath.Join(tmpdir, "deep/deep"))
if err != nil {
t.Errorf("unexpected error: %v", err)
} else if dirInfo.Mode().Perm() != os.FileMode(0747) {
t.Errorf("unexpected dir mode: %s", dirInfo.Mode())
}
}
func TestExtractTarFileToBuf(t *testing.T) {
entries := []*testTarEntry{
{
header: &tar.Header{
Name: "folder/",
Typeflag: tar.TypeDir,
Mode: int64(0747),
},
},
{
contents: "foo",
header: &tar.Header{
Name: "folder/foo.txt",
Size: 3,
},
},
{
contents: "bar",
header: &tar.Header{
Name: "folder/bar.txt",
Size: 3,
},
},
{
header: &tar.Header{
Name: "folder/symlink.txt",
Typeflag: tar.TypeSymlink,
Linkname: "folder/foo.txt",
},
},
}
testTarPath, err := newTestTar(entries)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.Remove(testTarPath)
containerTar1, err := os.Open(testTarPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer containerTar1.Close()
tr := tar.NewReader(containerTar1)
buf, err := extractFileFromTar(tr, "folder/foo.txt")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if string(buf) != "foo" {
t.Errorf("unexpected contents, wanted: %s, got: %s", "foo", buf)
}
containerTar2, err := os.Open(testTarPath)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer containerTar2.Close()
tr = tar.NewReader(containerTar2)
buf, err = extractFileFromTar(tr, "folder/symlink.txt")
if err == nil {
t.Errorf("expected error")
}
}
func TestExtractTarPWL(t *testing.T) {
if !sys.HasChrootCapability() {
t.Skipf("chroot capability not available. Disabling test.")
}
entries := []*testTarEntry{
{
header: &tar.Header{
Name: "folder/",
Typeflag: tar.TypeDir,
Mode: int64(0747),
},
},
{
contents: "foo",
header: &tar.Header{
Name: "folder/foo.txt",
Size: 3,
},
},
{
contents: "bar",
header: &tar.Header{
Name: "folder/bar.txt",
Size: 3,
},
},
{
header: &tar.Header{
Name: "folder/symlink.txt",
Typeflag: tar.TypeSymlink,
Linkname: "folder/foo.txt",
},
},
}
testTarPath, err := newTestTar(entries)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer os.Remove(testTarPath)
containerTar, err := os.Open(testTarPath)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer containerTar.Close()
tmpdir, err := ioutil.TempDir("", "rkt-temp-dir")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer os.RemoveAll(tmpdir)
pwl := make(PathWhitelistMap)
pwl["folder/foo.txt"] = struct{}{}
err = ExtractTar(containerTar, tmpdir, false, pwl)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
matches, err := filepath.Glob(filepath.Join(tmpdir, "folder/*.txt"))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(matches) != 1 {
t.Errorf("unexpected number of files found: %d, wanted 1", len(matches))
}
}
func TestExtractTarOverwrite(t *testing.T) {
if !sys.HasChrootCapability() {
t.Skipf("chroot capability not available. Disabling test.")
}
tmpdir, err := ioutil.TempDir("", "rkt-temp-dir")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(tmpdir)
entries := []*testTarEntry{
{
contents: "hello",
header: &tar.Header{
Name: "hello.txt",
Size: 5,
},
},
{
header: &tar.Header{
Name: "afolder",
Typeflag: tar.TypeDir,
},
},
{
contents: "hello",
header: &tar.Header{
Name: "afolder/hello.txt",
Size: 5,
},
},
{
contents: "hello",
header: &tar.Header{
Name: "afile",
Size: 5,
},
},
{
header: &tar.Header{
Name: "folder01",
Typeflag: tar.TypeDir,
},
},
{
contents: "hello",
header: &tar.Header{
Name: "folder01/file01",
Size: 5,
},
},
{
contents: "hello",
header: &tar.Header{
Name: "filesymlinked",
Size: 5,
},
},
{
header: &tar.Header{
Name: "linktofile",
Linkname: "filesymlinked",
Typeflag: tar.TypeSymlink,
},
},
{
header: &tar.Header{
Name: "dirsymlinked",
Typeflag: tar.TypeDir,
},
},
{
header: &tar.Header{
Name: "linktodir",
Linkname: "dirsymlinked",
Typeflag: tar.TypeSymlink,
},
},
}
testTarPath, err := newTestTar(entries)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.Remove(testTarPath)
containerTar1, err := os.Open(testTarPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer containerTar1.Close()
err = ExtractTar(containerTar1, tmpdir, false, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Now overwrite:
// a file with a new file
// a dir with a file
entries = []*testTarEntry{
{
contents: "newhello",
header: &tar.Header{
Name: "hello.txt",
Size: 8,
},
},
// Now this is a file
{
contents: "nowafile",
header: &tar.Header{
Name: "afolder",
Typeflag: tar.TypeReg,
Size: 8,
},
},
// Now this is a dir
{
header: &tar.Header{
Name: "afile",
Typeflag: tar.TypeDir,
},
},
// Overwrite symlink to a file with a regular file
// the linked file shouldn't be removed
{
contents: "filereplacingsymlink",
header: &tar.Header{
Name: "linktofile",
Typeflag: tar.TypeReg,
Size: 20,
},
},
// Overwrite symlink to a dir with a regular file
// the linked directory and all its contents shouldn't be
// removed
{
contents: "filereplacingsymlink",
header: &tar.Header{
Name: "linktodir",
Typeflag: tar.TypeReg,
Size: 20,
},
},
// folder01 already exists and shouldn't be removed (keeping folder01/file01)
{
header: &tar.Header{
Name: "folder01",
Typeflag: tar.TypeDir,
Mode: int64(0755),
},
},
{
contents: "hello",
header: &tar.Header{
Name: "folder01/file02",
Size: 5,
Mode: int64(0644),
},
},
}
testTarPath, err = newTestTar(entries)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer os.Remove(testTarPath)
containerTar2, err := os.Open(testTarPath)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer containerTar2.Close()
err = ExtractTar(containerTar2, tmpdir, true, nil)
expectedFiles := []*fileInfo{
&fileInfo{path: "hello.txt", typeflag: tar.TypeReg, size: 8, contents: "newhello"},
&fileInfo{path: "linktofile", typeflag: tar.TypeReg, size: 20},
&fileInfo{path: "linktodir", typeflag: tar.TypeReg, size: 20},
&fileInfo{path: "afolder", typeflag: tar.TypeReg, size: 8},
&fileInfo{path: "dirsymlinked", typeflag: tar.TypeDir},
&fileInfo{path: "afile", typeflag: tar.TypeDir},
&fileInfo{path: "filesymlinked", typeflag: tar.TypeReg, size: 5},
&fileInfo{path: "folder01", typeflag: tar.TypeDir},
&fileInfo{path: "folder01/file01", typeflag: tar.TypeReg, size: 5},
&fileInfo{path: "folder01/file02", typeflag: tar.TypeReg, size: 5},
}
err = checkExpectedFiles(tmpdir, fileInfoSliceToMap(expectedFiles))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestExtractTarTimes(t *testing.T) {
if !sys.HasChrootCapability() {
t.Skipf("chroot capability not available. Disabling test.")
}
// Do not set ns as tar has second precision
time1 := time.Unix(100000, 0)
time2 := time.Unix(200000, 0)
time3 := time.Unix(300000, 0)
entries := []*testTarEntry{
{
header: &tar.Header{
Name: "folder/",
Typeflag: tar.TypeDir,
Mode: int64(0747),
ModTime: time1,
},
},
{
contents: "foo",
header: &tar.Header{
Name: "folder/foo.txt",
Size: 3,
ModTime: time2,
},
},
{
header: &tar.Header{
Name: "folder/symlink.txt",
Typeflag: tar.TypeSymlink,
Linkname: "folder/foo.txt",
ModTime: time3,
},
},
}
testTarPath, err := newTestTar(entries)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer os.Remove(testTarPath)
containerTar, err := os.Open(testTarPath)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
defer containerTar.Close()
tmpdir, err := ioutil.TempDir("", "rkt-temp-dir")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
os.RemoveAll(tmpdir)
err = os.MkdirAll(tmpdir, 0755)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = ExtractTar(containerTar, tmpdir, false, nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = checkTime(filepath.Join(tmpdir, "folder/"), time1)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = checkTime(filepath.Join(tmpdir, "folder/foo.txt"), time2)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
//Check only (by now) on linux
if runtime.GOOS == "linux" {
err = checkTime(filepath.Join(tmpdir, "folder/symlink.txt"), time3)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
}
func checkTime(path string, time time.Time) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
if info.ModTime() != time {
return fmt.Errorf("%s: info.ModTime: %s, different from expected time: %s", path, info.ModTime(), time)
}
return nil
}