Skip to content

Commit b884eb7

Browse files
authored
Add fs.ResolvePath to resolve symbolic links (#275)
* Add `fs.ResolvePath` to resolve symbolic links `filepath.EvalSymlinks` does not work well on Windows, and can enter infinite loops in certain situations and error out. Use Win32 API GetFinalPathNameByHandle to handle path resolution. Implementation based off on: containerd/containerd#5411 Signed-off-by: Hamza El-Saawy <[email protected]> * PR: types, documentation Signed-off-by: Hamza El-Saawy <[email protected]> * remove unneded constant groups Signed-off-by: Hamza El-Saawy <[email protected]> * Attempt normalized path first Update logic to try querying for normalized path initially, then use opened path if access is denied. Signed-off-by: Hamza El-Saawy <[email protected]> --------- Signed-off-by: Hamza El-Saawy <[email protected]>
1 parent 41915dc commit b884eb7

12 files changed

Lines changed: 650 additions & 36 deletions

File tree

internal/fs/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This package contains Win32 filesystem functionality.
2+
package fs

internal/fs/fs.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
//go:build windows
2+
3+
package fs
4+
5+
import (
6+
"golang.org/x/sys/windows"
7+
8+
"github.com/Microsoft/go-winio/internal/stringbuffer"
9+
)
10+
11+
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go fs.go
12+
13+
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
14+
//sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *syscall.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW
15+
16+
const NullHandle windows.Handle = 0
17+
18+
// AccessMask defines standard, specific, and generic rights.
19+
//
20+
// Bitmask:
21+
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
22+
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
23+
// +---------------+---------------+-------------------------------+
24+
// |G|G|G|G|Resvd|A| StandardRights| SpecificRights |
25+
// |R|W|E|A| |S| | |
26+
// +-+-------------+---------------+-------------------------------+
27+
//
28+
// GR Generic Read
29+
// GW Generic Write
30+
// GE Generic Exectue
31+
// GA Generic All
32+
// Resvd Reserved
33+
// AS Access Security System
34+
//
35+
// https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask
36+
//
37+
// https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights
38+
//
39+
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants
40+
type AccessMask = windows.ACCESS_MASK
41+
42+
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
43+
const (
44+
// Not actually any.
45+
//
46+
// For CreateFile: "query certain metadata such as file, directory, or device attributes without accessing that file or device"
47+
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters
48+
FILE_ANY_ACCESS AccessMask = 0
49+
50+
// Specific Object Access
51+
// from ntioapi.h
52+
53+
FILE_READ_DATA AccessMask = (0x0001) // file & pipe
54+
FILE_LIST_DIRECTORY AccessMask = (0x0001) // directory
55+
56+
FILE_WRITE_DATA AccessMask = (0x0002) // file & pipe
57+
FILE_ADD_FILE AccessMask = (0x0002) // directory
58+
59+
FILE_APPEND_DATA AccessMask = (0x0004) // file
60+
FILE_ADD_SUBDIRECTORY AccessMask = (0x0004) // directory
61+
FILE_CREATE_PIPE_INSTANCE AccessMask = (0x0004) // named pipe
62+
63+
FILE_READ_EA AccessMask = (0x0008) // file & directory
64+
FILE_READ_PROPERTIES AccessMask = FILE_READ_EA
65+
66+
FILE_WRITE_EA AccessMask = (0x0010) // file & directory
67+
FILE_WRITE_PROPERTIES AccessMask = FILE_WRITE_EA
68+
69+
FILE_EXECUTE AccessMask = (0x0020) // file
70+
FILE_TRAVERSE AccessMask = (0x0020) // directory
71+
72+
FILE_DELETE_CHILD AccessMask = (0x0040) // directory
73+
74+
FILE_READ_ATTRIBUTES AccessMask = (0x0080) // all
75+
76+
FILE_WRITE_ATTRIBUTES AccessMask = (0x0100) // all
77+
78+
FILE_ALL_ACCESS AccessMask = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF)
79+
FILE_GENERIC_READ AccessMask = (STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE)
80+
FILE_GENERIC_WRITE AccessMask = (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE)
81+
FILE_GENERIC_EXECUTE AccessMask = (STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE)
82+
83+
SPECIFIC_RIGHTS_ALL AccessMask = 0x0000FFFF
84+
85+
// Standard Access
86+
// from ntseapi.h
87+
88+
DELETE AccessMask = 0x0001_0000
89+
READ_CONTROL AccessMask = 0x0002_0000
90+
WRITE_DAC AccessMask = 0x0004_0000
91+
WRITE_OWNER AccessMask = 0x0008_0000
92+
SYNCHRONIZE AccessMask = 0x0010_0000
93+
94+
STANDARD_RIGHTS_REQUIRED AccessMask = 0x000F_0000
95+
96+
STANDARD_RIGHTS_READ AccessMask = READ_CONTROL
97+
STANDARD_RIGHTS_WRITE AccessMask = READ_CONTROL
98+
STANDARD_RIGHTS_EXECUTE AccessMask = READ_CONTROL
99+
100+
STANDARD_RIGHTS_ALL AccessMask = 0x001F_0000
101+
)
102+
103+
type FileShareMode uint32
104+
105+
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
106+
const (
107+
FILE_SHARE_NONE FileShareMode = 0x00
108+
FILE_SHARE_READ FileShareMode = 0x01
109+
FILE_SHARE_WRITE FileShareMode = 0x02
110+
FILE_SHARE_DELETE FileShareMode = 0x04
111+
FILE_SHARE_VALID_FLAGS FileShareMode = 0x07
112+
)
113+
114+
type FileCreationDisposition uint32
115+
116+
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
117+
const (
118+
// from winbase.h
119+
120+
CREATE_NEW FileCreationDisposition = 0x01
121+
CREATE_ALWAYS FileCreationDisposition = 0x02
122+
OPEN_EXISTING FileCreationDisposition = 0x03
123+
OPEN_ALWAYS FileCreationDisposition = 0x04
124+
TRUNCATE_EXISTING FileCreationDisposition = 0x05
125+
)
126+
127+
// CreateFile and co. take flags or attributes together as one parameter.
128+
// Define alias until we can use generics to allow both
129+
130+
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
131+
type FileFlagOrAttribute uint32
132+
133+
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
134+
const ( // from winnt.h
135+
FILE_FLAG_WRITE_THROUGH FileFlagOrAttribute = 0x8000_0000
136+
FILE_FLAG_OVERLAPPED FileFlagOrAttribute = 0x4000_0000
137+
FILE_FLAG_NO_BUFFERING FileFlagOrAttribute = 0x2000_0000
138+
FILE_FLAG_RANDOM_ACCESS FileFlagOrAttribute = 0x1000_0000
139+
FILE_FLAG_SEQUENTIAL_SCAN FileFlagOrAttribute = 0x0800_0000
140+
FILE_FLAG_DELETE_ON_CLOSE FileFlagOrAttribute = 0x0400_0000
141+
FILE_FLAG_BACKUP_SEMANTICS FileFlagOrAttribute = 0x0200_0000
142+
FILE_FLAG_POSIX_SEMANTICS FileFlagOrAttribute = 0x0100_0000
143+
FILE_FLAG_OPEN_REPARSE_POINT FileFlagOrAttribute = 0x0020_0000
144+
FILE_FLAG_OPEN_NO_RECALL FileFlagOrAttribute = 0x0010_0000
145+
FILE_FLAG_FIRST_PIPE_INSTANCE FileFlagOrAttribute = 0x0008_0000
146+
)
147+
148+
type FileSQSFlag = FileFlagOrAttribute
149+
150+
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
151+
const ( // from winbase.h
152+
SECURITY_ANONYMOUS FileSQSFlag = FileSQSFlag(SecurityAnonymous << 16)
153+
SECURITY_IDENTIFICATION FileSQSFlag = FileSQSFlag(SecurityIdentification << 16)
154+
SECURITY_IMPERSONATION FileSQSFlag = FileSQSFlag(SecurityImpersonation << 16)
155+
SECURITY_DELEGATION FileSQSFlag = FileSQSFlag(SecurityDelegation << 16)
156+
157+
SECURITY_SQOS_PRESENT FileSQSFlag = 0x00100000
158+
SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F0000
159+
)
160+
161+
// GetFinalPathNameByHandle flags
162+
//
163+
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew#parameters
164+
type GetFinalPathFlag uint32
165+
166+
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
167+
const (
168+
GetFinalPathDefaultFlag GetFinalPathFlag = 0x0
169+
170+
FILE_NAME_NORMALIZED GetFinalPathFlag = 0x0
171+
FILE_NAME_OPENED GetFinalPathFlag = 0x8
172+
173+
VOLUME_NAME_DOS GetFinalPathFlag = 0x0
174+
VOLUME_NAME_GUID GetFinalPathFlag = 0x1
175+
VOLUME_NAME_NT GetFinalPathFlag = 0x2
176+
VOLUME_NAME_NONE GetFinalPathFlag = 0x4
177+
)
178+
179+
// getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle
180+
// with the given handle and flags. It transparently takes care of creating a buffer of the
181+
// correct size for the call.
182+
//
183+
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
184+
func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) {
185+
b := stringbuffer.NewWString()
186+
//TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n?
187+
for {
188+
n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags))
189+
if err != nil {
190+
return "", err
191+
}
192+
// If the buffer wasn't large enough, n will be the total size needed (including null terminator).
193+
// Resize and try again.
194+
if n > b.Cap() {
195+
b.ResizeTo(n)
196+
continue
197+
}
198+
// If the buffer is large enough, n will be the size not including the null terminator.
199+
// Convert to a Go string and return.
200+
return b.String(), nil
201+
}
202+
}

internal/fs/fs_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//go:build windows
2+
3+
package fs
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
"golang.org/x/sys/windows"
12+
)
13+
14+
func Test_GetFinalPathNameByHandle(t *testing.T) {
15+
d := t.TempDir()
16+
// open f via a relative path
17+
name := t.Name() + ".txt"
18+
fullPath := filepath.Join(d, name)
19+
20+
w, err := os.Getwd()
21+
if err != nil {
22+
t.Fatalf("could not get working directory: %v", err)
23+
}
24+
if err := os.Chdir(d); err != nil {
25+
t.Fatalf("could not chdir to %s: %v", d, err)
26+
}
27+
defer os.Chdir(w) //nolint:errcheck
28+
29+
f, err := os.Create(name)
30+
if err != nil {
31+
t.Fatalf("could not open %s: %v", fullPath, err)
32+
}
33+
defer f.Close()
34+
35+
path, err := GetFinalPathNameByHandle(windows.Handle(f.Fd()), GetFinalPathDefaultFlag)
36+
if err != nil {
37+
t.Fatalf("could not get final path for %s: %v", fullPath, err)
38+
}
39+
if strings.EqualFold(fullPath, path) {
40+
t.Fatalf("expected %s, got %s", fullPath, path)
41+
}
42+
}

internal/fs/security.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package fs
2+
3+
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level
4+
type SecurityImpersonationLevel int32 // C default enums underlying type is `int`, which is Go `int32`
5+
6+
// Impersonation levels
7+
const (
8+
SecurityAnonymous SecurityImpersonationLevel = 0
9+
SecurityIdentification SecurityImpersonationLevel = 1
10+
SecurityImpersonation SecurityImpersonationLevel = 2
11+
SecurityDelegation SecurityImpersonationLevel = 3
12+
)

internal/fs/zsyscall_windows.go

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)