Skip to content

Safer use of filepath.EvalSymlinks() on Windows #25151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion pkg/machine/machine_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,36 @@ func FindExecutablePeer(name string) (string, error) {
return "", err
}

exe, err = filepath.EvalSymlinks(exe)
exe, err = EvalSymlinksOrClean(exe)
if err != nil {
return "", err
}

return filepath.Join(filepath.Dir(exe), name), nil
}

func EvalSymlinksOrClean(filePath string) (string, error) {
fileInfo, err := os.Lstat(filePath)
if err != nil {
return "", err
}
if fileInfo.Mode()&fs.ModeSymlink != 0 {
// Only call filepath.EvalSymlinks if it is a symlink.
// Starting with v1.23, EvalSymlinks returns an error for mount points.
// See https://go-review.googlesource.com/c/go/+/565136 for reference.
filePath, err = filepath.EvalSymlinks(filePath)
if err != nil {
return "", err
}
} else {
// Call filepath.Clean when filePath is not a symlink. That's for
// consistency with the symlink case (filepath.EvalSymlinks calls
// Clean after evaluating filePath).
filePath = filepath.Clean(filePath)
}
return filePath, nil
}

func GetWinProxyStateDir(name string, vmtype define.VMType) (string, error) {
dir, err := env.GetDataDir(vmtype)
if err != nil {
Expand Down
116 changes: 116 additions & 0 deletions pkg/machine/machine_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//go:build windows

package machine

import (
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// CreateNewItemWithPowerShell creates a new item using PowerShell.
// It's an helper to easily create junctions on Windows (as well as other file types).
// It constructs a PowerShell command to create a new item at the specified path with the given item type.
// If a target is provided, it includes it in the command.
//
// Parameters:
// - t: The testing.T instance.
// - path: The path where the new item will be created.
// - itemType: The type of the item to be created (e.g., "File", "SymbolicLink", "Junction").
// - target: The target for the new item, if applicable.
func CreateNewItemWithPowerShell(t *testing.T, path string, itemType string, target string) {
var pwshCmd string
pwshCmd = "New-Item -Path " + path + " -ItemType " + itemType
if target != "" {
pwshCmd += " -Target " + target
}
cmd := exec.Command("pwsh", "-Command", pwshCmd)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
require.NoError(t, err)
}

// TestEvalSymlinksOrClean tests the EvalSymlinksOrClean function.
// In particular it verifies that EvalSymlinksOrClean behaves as
// filepath.EvalSymlink before Go 1.23 - with the exception of
// files under a mount point (juntion) that aren't resolved
// anymore.
// The old behavior of filepath.EvalSymlinks can be tested with
// the directive "//go:debug winsymlink=0" and replacing EvalSymlinksOrClean()
// with filepath.EvalSymlink().
func TestEvalSymlinksOrClean(t *testing.T) {
// Create a temporary directory to store the normal file
normalFileDir := t.TempDir()

// Create a temporary directory to store the (hard/sym)link files
linkFilesDir := t.TempDir()

// Create a temporary directory where the mount point will be created
mountPointDir := t.TempDir()

// Create a normal file
normalFile := filepath.Join(normalFileDir, "testFile")
CreateNewItemWithPowerShell(t, normalFile, "File", "")

// Create a symlink file
symlinkFile := filepath.Join(linkFilesDir, "testSymbolicLink")
CreateNewItemWithPowerShell(t, symlinkFile, "SymbolicLink", normalFile)

// Create a hardlink file
hardlinkFile := filepath.Join(linkFilesDir, "testHardLink")
CreateNewItemWithPowerShell(t, hardlinkFile, "HardLink", normalFile)

// Create a mount point file
mountPoint := filepath.Join(mountPointDir, "testJunction")
mountPointFile := filepath.Join(mountPoint, "testFile")
CreateNewItemWithPowerShell(t, mountPoint, "Junction", normalFileDir)

// Replaces the backslashes with forward slashes in the normal file path
normalFileWithBadSeparators := filepath.ToSlash(normalFile)

tests := []struct {
name string
filePath string
want string
}{
{
name: "Normal file",
filePath: normalFile,
want: normalFile,
},
{
name: "File under a mount point (juntion)",
filePath: mountPointFile,
want: mountPointFile,
},
{
name: "Symbolic link",
filePath: symlinkFile,
want: normalFile,
},
{
name: "Hard link",
filePath: hardlinkFile,
want: hardlinkFile,
},
{
name: "Bad separators in path",
filePath: normalFileWithBadSeparators,
want: normalFile,
},
}

for _, tt := range tests {
assert := assert.New(t)
t.Run(tt.name, func(t *testing.T) {
got, err := EvalSymlinksOrClean(tt.filePath)
require.NoError(t, err)
assert.Equal(tt.want, got)
})
}
}