Skip to content

Commit

Permalink
Safer use of filepath.EvalSymlinks() on Windows
Browse files Browse the repository at this point in the history
The behavior of function `path/filepath.EvalSymlinks()` has
changed in Go v1.23:

- https://go-review.googlesource.com/c/go/+/565136
- https://go.dev/doc/go1.23#minor_library_changes
- https://tip.golang.org/doc/godebug

As a consequences, starting with Podman 5.3.0, when installing
on Windows (WSL) using scoop, Podman fails to start because it
fails to find helper binaries. Scoop copies Podman binaries in
a folder of type Junction and `EvalSymlinks` returns an error.
The problem is described in containers#24557.

To address this problem we are checking if a path is a `Symlink`
before calling `EvalSymlinks` and, if it's not (hardlinks, mount
points or canonical files), we are calling `path/filepath.Clean`
for consistency. In fact `path/filepath.EvalSymlinks`, after
evaluating a symlink target, calls `Clean` too.

Signed-off-by: Mario Loriedo <[email protected]>
  • Loading branch information
l0rd committed Jan 29, 2025
1 parent eea2866 commit 0abbe02
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 1 deletion.
22 changes: 21 additions & 1 deletion pkg/machine/machine_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,34 @@ 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 the Path is not a symlink
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
137 changes: 137 additions & 0 deletions pkg/machine/machine_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//go:build windows

package machine

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

// 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:
// - 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(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)
if err := cmd.Run(); err != nil {
log.Fatal(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, err := os.MkdirTemp("", "")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(normalFileDir)

// Create a temporary directory to store the (hard/sym)link files
linkFilesDir, err := os.MkdirTemp("", "")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(linkFilesDir)

// Create a temporary directory where the mount point will be created
mountPointDir, err := os.MkdirTemp("", "")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(mountPointDir)

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

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

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

// Create a mount point file
mountPoint := filepath.Join(mountPointDir, "testJunction")
mountPointFile := filepath.Join(mountPoint, "testFile")
CreateNewItemWithPowerShell(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
wantErr bool
}{
{
name: "Normal file",
filePath: normalFile,
want: normalFile,
wantErr: false,
},
{
name: "File under a mount point (juntion)",
filePath: mountPointFile,
want: mountPointFile,
wantErr: false,
},
{
name: "Symbolic link",
filePath: symlinkFile,
want: normalFile,
wantErr: false,
},
{
name: "Hard link",
filePath: hardlinkFile,
want: hardlinkFile,
wantErr: false,
},
{
name: "Bad separators in path",
filePath: normalFileWithBadSeparators,
want: normalFile,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := EvalSymlinksOrClean(tt.filePath)
if err != nil && !tt.wantErr {
t.Errorf("EvalSymlinksOrClean() failed: %v", err)
return
}
if err == nil && tt.wantErr {
t.Fatal("EvalSymlinksOrClean() succeeded unexpectedly")
}
if got != tt.want {
t.Errorf("EvalSymlinksOrClean() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 0abbe02

Please sign in to comment.