Skip to content

Commit

Permalink
Merge pull request #1786 from hashicorp/b-fs-enforce
Browse files Browse the repository at this point in the history
Do not allow path to escape the alloc dir for the FS commands
  • Loading branch information
dadgar committed Oct 6, 2016
2 parents 483d8f9 + bcb1a2e commit 3d6a49e
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 8 deletions.
36 changes: 33 additions & 3 deletions client/allocdir/alloc_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ type AllocDirFS interface {
Stat(path string) (*AllocFileInfo, error)
ReadAt(path string, offset int64) (io.ReadCloser, error)
Snapshot(w io.Writer) error
BlockUntilExists(path string, t *tomb.Tomb) chan error
BlockUntilExists(path string, t *tomb.Tomb) (chan error, error)
ChangeEvents(path string, curOffset int64, t *tomb.Tomb) (*watch.FileChanges, error)
}

Expand Down Expand Up @@ -459,6 +459,12 @@ func (d *AllocDir) LogDir() string {

// List returns the list of files at a path relative to the alloc dir
func (d *AllocDir) List(path string) ([]*AllocFileInfo, error) {
if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
} else if escapes {
return nil, fmt.Errorf("Path escapes the alloc directory")
}

p := filepath.Join(d.AllocDir, path)
finfos, err := ioutil.ReadDir(p)
if err != nil {
Expand All @@ -479,6 +485,12 @@ func (d *AllocDir) List(path string) ([]*AllocFileInfo, error) {

// Stat returns information about the file at a path relative to the alloc dir
func (d *AllocDir) Stat(path string) (*AllocFileInfo, error) {
if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
} else if escapes {
return nil, fmt.Errorf("Path escapes the alloc directory")
}

p := filepath.Join(d.AllocDir, path)
info, err := os.Stat(p)
if err != nil {
Expand All @@ -496,6 +508,12 @@ func (d *AllocDir) Stat(path string) (*AllocFileInfo, error) {

// ReadAt returns a reader for a file at the path relative to the alloc dir
func (d *AllocDir) ReadAt(path string, offset int64) (io.ReadCloser, error) {
if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
} else if escapes {
return nil, fmt.Errorf("Path escapes the alloc directory")
}

p := filepath.Join(d.AllocDir, path)
f, err := os.Open(p)
if err != nil {
Expand All @@ -509,7 +527,13 @@ func (d *AllocDir) ReadAt(path string, offset int64) (io.ReadCloser, error) {

// BlockUntilExists blocks until the passed file relative the allocation
// directory exists. The block can be cancelled with the passed tomb.
func (d *AllocDir) BlockUntilExists(path string, t *tomb.Tomb) chan error {
func (d *AllocDir) BlockUntilExists(path string, t *tomb.Tomb) (chan error, error) {
if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
} else if escapes {
return nil, fmt.Errorf("Path escapes the alloc directory")
}

// Get the path relative to the alloc directory
p := filepath.Join(d.AllocDir, path)
watcher := getFileWatcher(p)
Expand All @@ -518,13 +542,19 @@ func (d *AllocDir) BlockUntilExists(path string, t *tomb.Tomb) chan error {
returnCh <- watcher.BlockUntilExists(t)
close(returnCh)
}()
return returnCh
return returnCh, nil
}

// ChangeEvents watches for changes to the passed path relative to the
// allocation directory. The offset should be the last read offset. The tomb is
// used to clean up the watch.
func (d *AllocDir) ChangeEvents(path string, curOffset int64, t *tomb.Tomb) (*watch.FileChanges, error) {
if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
} else if escapes {
return nil, fmt.Errorf("Path escapes the alloc directory")
}

// Get the path relative to the alloc directory
p := filepath.Join(d.AllocDir, path)
watcher := getFileWatcher(p)
Expand Down
45 changes: 45 additions & 0 deletions client/allocdir/alloc_dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import (
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"

tomb "gopkg.in/tomb.v1"

"github.com/hashicorp/nomad/client/testutil"
"github.com/hashicorp/nomad/nomad/structs"
)
Expand Down Expand Up @@ -345,3 +348,45 @@ func TestAllocDir_Move(t *testing.T) {
t.Fatalf("task local dir was not moved")
}
}

func TestAllocDir_EscapeChecking(t *testing.T) {
tmp, err := ioutil.TempDir("", "AllocDir")
if err != nil {
t.Fatalf("Couldn't create temp dir: %v", err)
}
defer os.RemoveAll(tmp)

d := NewAllocDir(tmp, structs.DefaultResources().DiskMB)
defer d.Destroy()
tasks := []*structs.Task{t1, t2}
if err := d.Build(tasks); err != nil {
t.Fatalf("Build(%v) failed: %v", tasks, err)
}

// Check that issuing calls that escape the alloc dir returns errors
// List
if _, err := d.List(".."); err == nil || !strings.Contains(err.Error(), "escapes") {
t.Fatalf("List of escaping path didn't error: %v", err)
}

// Stat
if _, err := d.Stat("../foo"); err == nil || !strings.Contains(err.Error(), "escapes") {
t.Fatalf("Stat of escaping path didn't error: %v", err)
}

// ReadAt
if _, err := d.ReadAt("../foo", 0); err == nil || !strings.Contains(err.Error(), "escapes") {
t.Fatalf("ReadAt of escaping path didn't error: %v", err)
}

// BlockUntilExists
tomb := tomb.Tomb{}
if _, err := d.BlockUntilExists("../foo", &tomb); err == nil || !strings.Contains(err.Error(), "escapes") {
t.Fatalf("BlockUntilExists of escaping path didn't error: %v", err)
}

// ChangeEvents
if _, err := d.ChangeEvents("../foo", 0, &tomb); err == nil || !strings.Contains(err.Error(), "escapes") {
t.Fatalf("ChangeEvents of escaping path didn't error: %v", err)
}
}
8 changes: 7 additions & 1 deletion command/agent/fs_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,13 @@ func blockUntilNextLog(fs allocdir.AllocDirFS, t *tomb.Tomb, logPath, task, logT
next := make(chan error, 1)

go func() {
eofCancelCh := fs.BlockUntilExists(nextPath, t)
eofCancelCh, err := fs.BlockUntilExists(nextPath, t)
if err != nil {
next <- err
close(next)
return
}

scanCh := time.Tick(nextLogCheckRate)
for {
select {
Expand Down
8 changes: 4 additions & 4 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2274,7 +2274,7 @@ func (t *Template) Validate() error {
}

// Verify the destination doesn't escape
escaped, err := pathEscapesAllocDir(t.DestPath)
escaped, err := PathEscapesAllocDir(t.DestPath)
if err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid destination path: %v", err))
} else if escaped {
Expand Down Expand Up @@ -2593,9 +2593,9 @@ func (ta *TaskArtifact) GoString() string {
return fmt.Sprintf("%+v", ta)
}

// pathEscapesAllocDir returns if the given path escapes the allocation
// PathEscapesAllocDir returns if the given path escapes the allocation
// directory
func pathEscapesAllocDir(path string) (bool, error) {
func PathEscapesAllocDir(path string) (bool, error) {
// Verify the destination doesn't escape the tasks directory
alloc, err := filepath.Abs(filepath.Join("/", "foo/", "bar/"))
if err != nil {
Expand All @@ -2620,7 +2620,7 @@ func (ta *TaskArtifact) Validate() error {
mErr.Errors = append(mErr.Errors, fmt.Errorf("source must be specified"))
}

escaped, err := pathEscapesAllocDir(ta.RelativeDest)
escaped, err := PathEscapesAllocDir(ta.RelativeDest)
if err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid destination path: %v", err))
} else if escaped {
Expand Down

0 comments on commit 3d6a49e

Please sign in to comment.