dotcloud/docker

View on GitHub
internal/safepath/join_windows.go

Summary

Maintainability
A
1 hr
Test Coverage
package safepath

import (
    "context"
    "os"
    "path/filepath"
    "strings"

    "github.com/containerd/log"
    "github.com/docker/docker/internal/cleanups"
    "github.com/pkg/errors"
    "golang.org/x/sys/windows"
)

// Join locks all individual components of the path which is the concatenation
// of provided path and its subpath, checks that it doesn't escape the base path
// and returns the concatenated path.
//
// The path is safe (the path target won't change) until the returned SafePath
// is Closed.
// Caller is responsible for calling the Close function which unlocks the path.
func Join(ctx context.Context, path, subpath string) (*SafePath, error) {
    base, subpart, err := evaluatePath(path, subpath)
    if err != nil {
        return nil, err
    }
    parts := strings.Split(subpart, string(os.PathSeparator))

    cleanupJobs := cleanups.Composite{}
    defer func() {
        if cErr := cleanupJobs.Call(context.WithoutCancel(ctx)); cErr != nil {
            log.G(ctx).WithError(cErr).Warn("failed to close handles after error")
        }
    }()

    fullPath := base
    for _, part := range parts {
        fullPath = filepath.Join(fullPath, part)

        handle, err := lockFile(fullPath)
        if err != nil {
            if errors.Is(err, windows.ERROR_FILE_NOT_FOUND) {
                return nil, &ErrNotAccessible{Path: fullPath, Cause: err}
            }
            return nil, errors.Wrapf(err, "failed to lock file %s", fullPath)
        }
        cleanupJobs.Add(func(context.Context) error {
            if err := windows.CloseHandle(handle); err != nil {
                return &os.PathError{Op: "CloseHandle", Path: fullPath, Err: err}
            }
            return err
        })

        realPath, err := filepath.EvalSymlinks(fullPath)
        if err != nil {
            return nil, errors.Wrapf(err, "failed to eval symlinks of %s", fullPath)
        }

        if realPath != fullPath && !isLocalTo(realPath, base) {
            return nil, &ErrEscapesBase{Base: base, Subpath: subpart}
        }

        var info windows.ByHandleFileInformation
        if err := windows.GetFileInformationByHandle(handle, &info); err != nil {
            return nil, errors.WithStack(&os.PathError{Op: "GetFileInformationByHandle", Path: fullPath, Err: err})
        }

        if (info.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT) != 0 {
            return nil, &ErrNotAccessible{Path: fullPath, Cause: err}
        }
    }

    return &SafePath{
        path:          fullPath,
        sourceBase:    base,
        sourceSubpath: subpart,
        cleanup:       cleanupJobs.Release(),
    }, nil
}

func lockFile(path string) (windows.Handle, error) {
    p, err := windows.UTF16PtrFromString(path)
    if err != nil {
        return windows.InvalidHandle, &os.PathError{Op: "UTF16PtrFromString", Path: path, Err: err}
    }
    const flags = windows.FILE_FLAG_BACKUP_SEMANTICS | windows.FILE_FLAG_OPEN_REPARSE_POINT
    handle, err := windows.CreateFile(p, windows.GENERIC_READ, windows.FILE_SHARE_READ, nil, windows.OPEN_EXISTING, flags, 0)
    if err != nil {
        return handle, &os.PathError{Op: "CreateFile", Path: path, Err: err}
    }
    return handle, nil
}