Skip to content

Commit d1119dc

Browse files
authored
Merge pull request #5537 from kevpar/symlink-1.5
[release/1.5] cherry-pick: windows: Use GetFinalPathNameByHandle for ResolveSymbolicLink
2 parents 36cc874 + cba7b44 commit d1119dc

4 files changed

Lines changed: 453 additions & 13 deletions

File tree

pkg/os/os.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"io"
2121
"io/ioutil"
2222
"os"
23-
"path/filepath"
2423

2524
"github.com/moby/sys/symlink"
2625
)
@@ -56,18 +55,6 @@ func (RealOS) Stat(name string) (os.FileInfo, error) {
5655
return os.Stat(name)
5756
}
5857

59-
// ResolveSymbolicLink will follow any symbolic links
60-
func (RealOS) ResolveSymbolicLink(path string) (string, error) {
61-
info, err := os.Lstat(path)
62-
if err != nil {
63-
return "", err
64-
}
65-
if info.Mode()&os.ModeSymlink != os.ModeSymlink {
66-
return path, nil
67-
}
68-
return filepath.EvalSymlinks(path)
69-
}
70-
7158
// FollowSymlinkInScope will call symlink.FollowSymlinkInScope.
7259
func (RealOS) FollowSymlinkInScope(path, scope string) (string, error) {
7360
return symlink.FollowSymlinkInScope(path, scope)

pkg/os/os_unix.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
package os
2020

2121
import (
22+
"os"
23+
"path/filepath"
24+
2225
"github.com/containerd/containerd/mount"
2326
)
2427

@@ -29,3 +32,15 @@ type UNIX interface {
2932
Unmount(target string) error
3033
LookupMount(path string) (mount.Info, error)
3134
}
35+
36+
// ResolveSymbolicLink will follow any symbolic links
37+
func (RealOS) ResolveSymbolicLink(path string) (string, error) {
38+
info, err := os.Lstat(path)
39+
if err != nil {
40+
return "", err
41+
}
42+
if info.Mode()&os.ModeSymlink != os.ModeSymlink {
43+
return path, nil
44+
}
45+
return filepath.EvalSymlinks(path)
46+
}

pkg/os/os_windows.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package os
18+
19+
import (
20+
"os"
21+
"strings"
22+
"sync"
23+
"unicode/utf16"
24+
25+
"golang.org/x/sys/windows"
26+
)
27+
28+
// openPath takes a path, opens it, and returns the resulting handle.
29+
// It works for both file and directory paths.
30+
//
31+
// We are not able to use builtin Go functionality for opening a directory path:
32+
// - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile.
33+
// - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to
34+
// open a directory.
35+
// We could use os.Open if the path is a file, but it's easier to just use the same code for both.
36+
// Therefore, we call windows.CreateFile directly.
37+
func openPath(path string) (windows.Handle, error) {
38+
u16, err := windows.UTF16PtrFromString(path)
39+
if err != nil {
40+
return 0, err
41+
}
42+
h, err := windows.CreateFile(
43+
u16,
44+
0,
45+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
46+
nil,
47+
windows.OPEN_EXISTING,
48+
windows.FILE_FLAG_BACKUP_SEMANTICS, // Needed to open a directory handle.
49+
0)
50+
if err != nil {
51+
return 0, &os.PathError{
52+
Op: "CreateFile",
53+
Path: path,
54+
Err: err,
55+
}
56+
}
57+
return h, nil
58+
}
59+
60+
// GetFinalPathNameByHandle flags.
61+
//nolint:golint
62+
const (
63+
cFILE_NAME_OPENED = 0x8
64+
65+
cVOLUME_NAME_DOS = 0x0
66+
cVOLUME_NAME_GUID = 0x1
67+
)
68+
69+
var pool = sync.Pool{
70+
New: func() interface{} {
71+
// Size of buffer chosen somewhat arbitrarily to accommodate a large number of path strings.
72+
// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310.
73+
b := make([]uint16, 310)
74+
return &b
75+
},
76+
}
77+
78+
// getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle
79+
// with the given handle and flags. It transparently takes care of creating a buffer of the
80+
// correct size for the call.
81+
func getFinalPathNameByHandle(h windows.Handle, flags uint32) (string, error) {
82+
b := *(pool.Get().(*[]uint16))
83+
defer func() { pool.Put(&b) }()
84+
for {
85+
n, err := windows.GetFinalPathNameByHandle(h, &b[0], uint32(len(b)), flags)
86+
if err != nil {
87+
return "", err
88+
}
89+
// If the buffer wasn't large enough, n will be the total size needed (including null terminator).
90+
// Resize and try again.
91+
if n > uint32(len(b)) {
92+
b = make([]uint16, n)
93+
continue
94+
}
95+
// If the buffer is large enough, n will be the size not including the null terminator.
96+
// Convert to a Go string and return.
97+
return string(utf16.Decode(b[:n])), nil
98+
}
99+
}
100+
101+
// resolvePath implements path resolution for Windows. It attempts to return the "real" path to the
102+
// file or directory represented by the given path.
103+
// The resolution works by using the Windows API GetFinalPathNameByHandle, which takes a handle and
104+
// returns the final path to that file.
105+
func resolvePath(path string) (string, error) {
106+
h, err := openPath(path)
107+
if err != nil {
108+
return "", err
109+
}
110+
defer windows.CloseHandle(h)
111+
112+
// We use the Windows API GetFinalPathNameByHandle to handle path resolution. GetFinalPathNameByHandle
113+
// returns a resolved path name for a file or directory. The returned path can be in several different
114+
// formats, based on the flags passed. There are several goals behind the design here:
115+
// - Do as little manual path manipulation as possible. Since Windows path formatting can be quite
116+
// complex, we try to just let the Windows APIs handle that for us.
117+
// - Retain as much compatibility with existing Go path functions as we can. In particular, we try to
118+
// ensure paths returned from resolvePath can be passed to EvalSymlinks.
119+
//
120+
// First, we query for the VOLUME_NAME_GUID path of the file. This will return a path in the form
121+
// "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". If the path is a UNC share
122+
// (e.g. "\\server\share\dir\file.txt"), then the VOLUME_NAME_GUID query will fail with ERROR_PATH_NOT_FOUND.
123+
// In this case, we will next try a VOLUME_NAME_DOS query. This query will return a path for a UNC share
124+
// in the form "\\?\UNC\server\share\dir\file.txt". This path will work with most functions, but EvalSymlinks
125+
// fails on it. Therefore, we rewrite the path to the form "\\server\share\dir\file.txt" before returning it.
126+
// This path rewrite may not be valid in all cases (see the notes in the next paragraph), but those should
127+
// be very rare edge cases, and this case wouldn't have worked with EvalSymlinks anyways.
128+
//
129+
// The "\\?\" prefix indicates that no path parsing or normalization should be performed by Windows.
130+
// Instead the path is passed directly to the object manager. The lack of parsing means that "." and ".." are
131+
// interpreted literally and "\"" must be used as a path separator. Additionally, because normalization is
132+
// not done, certain paths can only be represented in this format. For instance, "\\?\C:\foo." (with a trailing .)
133+
// cannot be written as "C:\foo.", because path normalization will remove the trailing ".".
134+
//
135+
// We use FILE_NAME_OPENED instead of FILE_NAME_NORMALIZED, as FILE_NAME_NORMALIZED can fail on some
136+
// UNC paths based on access restrictions. The additional normalization done is also quite minimal in
137+
// most cases.
138+
//
139+
// Querying for VOLUME_NAME_DOS first instead of VOLUME_NAME_GUID would yield a "nicer looking" path in some cases.
140+
// For instance, it could return "\\?\C:\dir\file.txt" instead of "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt".
141+
// However, we query for VOLUME_NAME_GUID first for two reasons:
142+
// - The volume GUID path is more stable. A volume's mount point can change when it is remounted, but its
143+
// volume GUID should not change.
144+
// - If the volume is mounted at a non-drive letter path (e.g. mounted to "C:\mnt"), then VOLUME_NAME_DOS
145+
// will return the mount path. EvalSymlinks fails on a path like this due to a bug.
146+
//
147+
// References:
148+
// - GetFinalPathNameByHandle: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea
149+
// - Naming Files, Paths, and Namespaces: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
150+
// - Naming a Volume: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-volume
151+
152+
rPath, err := getFinalPathNameByHandle(h, cFILE_NAME_OPENED|cVOLUME_NAME_GUID)
153+
if err == windows.ERROR_PATH_NOT_FOUND {
154+
// ERROR_PATH_NOT_FOUND is returned from the VOLUME_NAME_GUID query if the path is a
155+
// network share (UNC path). In this case, query for the DOS name instead, then translate
156+
// the returned path to make it more palatable to other path functions.
157+
rPath, err = getFinalPathNameByHandle(h, cFILE_NAME_OPENED|cVOLUME_NAME_DOS)
158+
if err != nil {
159+
return "", err
160+
}
161+
if strings.HasPrefix(rPath, `\\?\UNC\`) {
162+
// Convert \\?\UNC\server\share -> \\server\share. The \\?\UNC syntax does not work with
163+
// some Go filepath functions such as EvalSymlinks. In the future if other components
164+
// move away from EvalSymlinks and use GetFinalPathNameByHandle instead, we could remove
165+
// this path munging.
166+
rPath = `\\` + rPath[len(`\\?\UNC\`):]
167+
}
168+
} else if err != nil {
169+
return "", err
170+
}
171+
return rPath, nil
172+
}
173+
174+
// ResolveSymbolicLink will follow any symbolic links
175+
func (RealOS) ResolveSymbolicLink(path string) (string, error) {
176+
// filepath.EvalSymlinks does not work very well on Windows, so instead we resolve the path
177+
// via resolvePath which uses GetFinalPathNameByHandle. This returns either a path prefixed with `\\?\`,
178+
// or a remote share path in the form \\server\share. These should work with most Go and Windows APIs.
179+
return resolvePath(path)
180+
}

0 commit comments

Comments
 (0)