Skip to content

Commit 6eb49ee

Browse files
committed
Safer use of filepath.EvalSymlinks() on Windows
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]>
1 parent eea2866 commit 6eb49ee

File tree

2 files changed

+160
-1
lines changed

2 files changed

+160
-1
lines changed

pkg/machine/machine_windows.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,14 +251,36 @@ func FindExecutablePeer(name string) (string, error) {
251251
return "", err
252252
}
253253

254-
exe, err = filepath.EvalSymlinks(exe)
254+
exe, err = EvalSymlinksOrClean(exe)
255255
if err != nil {
256256
return "", err
257257
}
258258

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

262+
func EvalSymlinksOrClean(filePath string) (string, error) {
263+
fileInfo, err := os.Lstat(filePath)
264+
if err != nil {
265+
return "", err
266+
}
267+
if fileInfo.Mode()&fs.ModeSymlink != 0 {
268+
// Only call filepath.EvalSymlinks if it is a symlink.
269+
// Starting with v1.23, EvalSymlinks returns an error for mount points.
270+
// See https://go-review.googlesource.com/c/go/+/565136 for reference.
271+
filePath, err = filepath.EvalSymlinks(filePath)
272+
if err != nil {
273+
return "", err
274+
}
275+
} else {
276+
// Call filepath.Clean when filePath is not a symlink. That's for
277+
// consistency with the symlink case (filepath.EvalSymlinks calls
278+
// Clean after evaluating filePath).
279+
filePath = filepath.Clean(filePath)
280+
}
281+
return filePath, nil
282+
}
283+
262284
func GetWinProxyStateDir(name string, vmtype define.VMType) (string, error) {
263285
dir, err := env.GetDataDir(vmtype)
264286
if err != nil {

pkg/machine/machine_windows_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//go:build windows
2+
3+
package machine
4+
5+
import (
6+
"log"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"testing"
11+
)
12+
13+
// CreateNewItemWithPowerShell creates a new item using PowerShell.
14+
// It's an helper to easily create junctions on Windows (as well as other file types).
15+
// It constructs a PowerShell command to create a new item at the specified path with the given item type.
16+
// If a target is provided, it includes it in the command.
17+
//
18+
// Parameters:
19+
// - path: The path where the new item will be created.
20+
// - itemType: The type of the item to be created (e.g., "File", "SymbolicLink", "Junction").
21+
// - target: The target for the new item, if applicable.
22+
func CreateNewItemWithPowerShell(path string, itemType string, target string) {
23+
var pwshCmd string
24+
pwshCmd = "New-Item -Path " + path + " -ItemType " + itemType
25+
if target != "" {
26+
pwshCmd += " -Target " + target
27+
}
28+
cmd := exec.Command("pwsh", "-Command", pwshCmd)
29+
if err := cmd.Run(); err != nil {
30+
log.Fatal(err)
31+
}
32+
}
33+
34+
// TestEvalSymlinksOrClean tests the EvalSymlinksOrClean function.
35+
// In particular it verifies that EvalSymlinksOrClean behaves as
36+
// filepath.EvalSymlink before Go 1.23 - with the exception of
37+
// files under a mount point (juntion) that aren't resolved
38+
// anymore.
39+
// The old behavior of filepath.EvalSymlinks can be tested with
40+
// the directive "//go:debug winsymlink=0" and replacing EvalSymlinksOrClean()
41+
// with filepath.EvalSymlink().
42+
func TestEvalSymlinksOrClean(t *testing.T) {
43+
// Create a temporary directory to store the normal file
44+
normalFileDir, err := os.MkdirTemp("", "")
45+
if err != nil {
46+
log.Fatal(err)
47+
}
48+
defer os.RemoveAll(normalFileDir)
49+
50+
// Create a temporary directory to store the (hard/sym)link files
51+
linkFilesDir, err := os.MkdirTemp("", "")
52+
if err != nil {
53+
log.Fatal(err)
54+
}
55+
defer os.RemoveAll(linkFilesDir)
56+
57+
// Create a temporary directory where the mount point will be created
58+
mountPointDir, err := os.MkdirTemp("", "")
59+
if err != nil {
60+
log.Fatal(err)
61+
}
62+
defer os.RemoveAll(mountPointDir)
63+
64+
// Create a normal file
65+
normalFile := filepath.Join(normalFileDir, "testFile")
66+
CreateNewItemWithPowerShell(normalFile, "File", "")
67+
68+
// Create a symlink file
69+
symlinkFile := filepath.Join(linkFilesDir, "testSymbolicLink")
70+
CreateNewItemWithPowerShell(symlinkFile, "SymbolicLink", normalFile)
71+
72+
// Create a hardlink file
73+
hardlinkFile := filepath.Join(linkFilesDir, "testHardLink")
74+
CreateNewItemWithPowerShell(hardlinkFile, "HardLink", normalFile)
75+
76+
// Create a mount point file
77+
mountPoint := filepath.Join(mountPointDir, "testJunction")
78+
mountPointFile := filepath.Join(mountPoint, "testFile")
79+
CreateNewItemWithPowerShell(mountPoint, "Junction", normalFileDir)
80+
81+
// Replaces the backslashes with forward slashes in the normal file path
82+
normalFileWithBadSeparators := filepath.ToSlash(normalFile)
83+
84+
tests := []struct {
85+
name string
86+
filePath string
87+
want string
88+
wantErr bool
89+
}{
90+
{
91+
name: "Normal file",
92+
filePath: normalFile,
93+
want: normalFile,
94+
wantErr: false,
95+
},
96+
{
97+
name: "File under a mount point (juntion)",
98+
filePath: mountPointFile,
99+
want: mountPointFile,
100+
wantErr: false,
101+
},
102+
{
103+
name: "Symbolic link",
104+
filePath: symlinkFile,
105+
want: normalFile,
106+
wantErr: false,
107+
},
108+
{
109+
name: "Hard link",
110+
filePath: hardlinkFile,
111+
want: hardlinkFile,
112+
wantErr: false,
113+
},
114+
{
115+
name: "Bad separators in path",
116+
filePath: normalFileWithBadSeparators,
117+
want: normalFile,
118+
wantErr: false,
119+
},
120+
}
121+
122+
for _, tt := range tests {
123+
t.Run(tt.name, func(t *testing.T) {
124+
got, err := EvalSymlinksOrClean(tt.filePath)
125+
if err != nil && !tt.wantErr {
126+
t.Errorf("EvalSymlinksOrClean() failed: %v", err)
127+
return
128+
}
129+
if err == nil && tt.wantErr {
130+
t.Fatal("EvalSymlinksOrClean() succeeded unexpectedly")
131+
}
132+
if got != tt.want {
133+
t.Errorf("EvalSymlinksOrClean() = %v, want %v", got, tt.want)
134+
}
135+
})
136+
}
137+
}

0 commit comments

Comments
 (0)