Skip to content

Commit b813465

Browse files
gdamsqmuntal
authored andcommitted
os: check for valid Windows path when creating files
Checks for a valid Windows path by ensuring the path doesn't end with trailing spaces or periods. Fixes #54040. Cq-Include-Trybots: luci.golang.try:gotip-windows-arm64 Change-Id: I266f79963c821f8cc474097d3e57c5645ad996fc Reviewed-on: https://go-review.googlesource.com/c/go/+/618496 Reviewed-by: Quim Muntal <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Alex Brainman <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent fdfb306 commit b813465

7 files changed

+203
-17
lines changed

src/os/export_windows_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ package os
77
// Export for testing.
88

99
var (
10-
AddExtendedPrefix = addExtendedPrefix
11-
NewConsoleFile = newConsoleFile
12-
CommandLineToArgv = commandLineToArgv
13-
AllowReadDirFileID = &allowReadDirFileID
10+
AddExtendedPrefix = addExtendedPrefix
11+
NewConsoleFile = newConsoleFile
12+
CommandLineToArgv = commandLineToArgv
13+
AllowReadDirFileID = &allowReadDirFileID
14+
ValidatePathForCreate = validatePathForCreate
1415
)

src/os/file.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -305,25 +305,18 @@ func (f *File) WriteString(s string) (n int, err error) {
305305
// bits (before umask).
306306
// If there is an error, it will be of type *PathError.
307307
func Mkdir(name string, perm FileMode) error {
308-
longName := fixLongPath(name)
309-
e := ignoringEINTR(func() error {
310-
return syscall.Mkdir(longName, syscallMode(perm))
311-
})
312-
313-
if e != nil {
314-
return &PathError{Op: "mkdir", Path: name, Err: e}
308+
err := mkdir(name, perm)
309+
if err != nil {
310+
return &PathError{Op: "mkdir", Path: name, Err: err}
315311
}
316-
317312
// mkdir(2) itself won't handle the sticky bit on *BSD and Solaris
318313
if !supportsCreateWithStickyBit && perm&ModeSticky != 0 {
319-
e = setStickyBit(name)
320-
321-
if e != nil {
314+
err = setStickyBit(name)
315+
if err != nil {
322316
Remove(name)
323-
return e
317+
return err
324318
}
325319
}
326-
327320
return nil
328321
}
329322

src/os/file_nonwindows.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !windows
6+
7+
package os
8+
9+
import "syscall"
10+
11+
func mkdir(name string, perm FileMode) error {
12+
return ignoringEINTR(func() error {
13+
return syscall.Mkdir(name, syscallMode(perm))
14+
})
15+
}

src/os/file_windows.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"unsafe"
1818
)
1919

20+
var errInvalidPath = errors.New("invalid path: cannot end with a space or period")
21+
2022
// This matches the value in syscall/syscall_windows.go.
2123
const _UTIME_OMIT = -1
2224

@@ -102,6 +104,9 @@ func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
102104
if name == "" {
103105
return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT}
104106
}
107+
if flag&O_CREATE != 0 && !validatePathForCreate(name) {
108+
return nil, &PathError{Op: "open", Path: name, Err: errInvalidPath}
109+
}
105110
path := fixLongPath(name)
106111
r, err := syscall.Open(path, flag|syscall.O_CLOEXEC, syscallMode(perm))
107112
if err != nil {
@@ -114,6 +119,14 @@ func openDirNolog(name string) (*File, error) {
114119
return openFileNolog(name, O_RDONLY, 0)
115120
}
116121

122+
func mkdir(name string, perm FileMode) error {
123+
if !validatePathForCreate(name) {
124+
return errInvalidPath
125+
}
126+
longName := fixLongPath(name)
127+
return syscall.Mkdir(longName, syscallMode(perm))
128+
}
129+
117130
func (file *file) close() error {
118131
if file == nil {
119132
return syscall.EINVAL
@@ -204,6 +217,9 @@ func Remove(name string) error {
204217
}
205218

206219
func rename(oldname, newname string) error {
220+
if !validatePathForCreate(newname) {
221+
return &LinkError{"rename", oldname, newname, errInvalidPath}
222+
}
207223
e := windows.Rename(fixLongPath(oldname), fixLongPath(newname))
208224
if e != nil {
209225
return &LinkError{"rename", oldname, newname, e}
@@ -252,6 +268,9 @@ func tempDir() string {
252268
// Link creates newname as a hard link to the oldname file.
253269
// If there is an error, it will be of type *LinkError.
254270
func Link(oldname, newname string) error {
271+
if !validatePathForCreate(newname) {
272+
return &LinkError{"link", oldname, newname, errInvalidPath}
273+
}
255274
n, err := syscall.UTF16PtrFromString(fixLongPath(newname))
256275
if err != nil {
257276
return &LinkError{"link", oldname, newname, err}
@@ -272,6 +291,9 @@ func Link(oldname, newname string) error {
272291
// if oldname is later created as a directory the symlink will not work.
273292
// If there is an error, it will be of type *LinkError.
274293
func Symlink(oldname, newname string) error {
294+
if !validatePathForCreate(newname) {
295+
return &LinkError{"symlink", oldname, newname, errInvalidPath}
296+
}
275297
// '/' does not work in link's content
276298
oldname = filepathlite.FromSlash(oldname)
277299

src/os/os_windows_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,3 +1561,76 @@ func TestReadDirNoFileID(t *testing.T) {
15611561
t.Errorf("SameFile(%v, %v) = false; want true", f2, f2s)
15621562
}
15631563
}
1564+
1565+
func TestOpen_InvalidPath(t *testing.T) {
1566+
dir := t.TempDir()
1567+
1568+
file, err := os.Open(dir + ".")
1569+
if err != nil {
1570+
t.Errorf("Open(%q) should have succeeded, got %v", dir+".", err)
1571+
} else {
1572+
file.Close()
1573+
}
1574+
1575+
file, err = os.Open(dir + " ")
1576+
if err != nil {
1577+
t.Errorf("Open(%q) should have succeeded, got %v", dir+" ", err)
1578+
} else {
1579+
file.Close()
1580+
}
1581+
}
1582+
1583+
func TestMkdirAll_InvalidPath(t *testing.T) {
1584+
// Parent folder contains traling spaces
1585+
path := `C:\temp\folder \this one fails`
1586+
err := os.MkdirAll(path, 0644)
1587+
if err == nil {
1588+
t.Errorf("MkdirAll(%q) should have failed", path)
1589+
} else if !strings.Contains(err.Error(), "invalid path: cannot end with a space or period") {
1590+
t.Errorf("expected errInvalidPath for path %q, got %v", path, err)
1591+
}
1592+
}
1593+
1594+
func TestCreate_InvalidPath(t *testing.T) {
1595+
testInvalidPath(t, func(_, path string) error {
1596+
_, err := os.Create(path)
1597+
return err
1598+
})
1599+
}
1600+
1601+
func TestMkdir_InvalidPath(t *testing.T) {
1602+
testInvalidPath(t, func(_, path string) error {
1603+
return os.Mkdir(path, 0644)
1604+
})
1605+
}
1606+
1607+
func TestRename_InvalidPath(t *testing.T) {
1608+
testInvalidPath(t, os.Rename)
1609+
}
1610+
1611+
func TestLink_InvalidPath(t *testing.T) {
1612+
testInvalidPath(t, os.Link)
1613+
}
1614+
1615+
func TestSymlink_InvalidPath(t *testing.T) {
1616+
testInvalidPath(t, os.Symlink)
1617+
}
1618+
1619+
func testInvalidPath(t *testing.T, fn func(src, dest string) error) {
1620+
dir := t.TempDir()
1621+
1622+
// Test invalid paths (with trailing space and period)
1623+
invalidPaths := []string{
1624+
filepath.Join(dir, "invalid_dir "), // path ending in space
1625+
filepath.Join(dir, "invalid_dir."), // path ending in period
1626+
}
1627+
1628+
for _, path := range invalidPaths {
1629+
err := fn(dir, path)
1630+
if err == nil {
1631+
t.Errorf("(%q, %q) should have failed", dir, path)
1632+
} else if !strings.Contains(err.Error(), "invalid path: cannot end with a space or period") {
1633+
t.Errorf("expected errInvalidPath for path %q, got %v", path, err)
1634+
}
1635+
}
1636+
}

src/os/path_windows.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package os
66

77
import (
88
"internal/filepathlite"
9+
"internal/stringslite"
910
"internal/syscall/windows"
1011
"syscall"
1112
)
@@ -150,3 +151,31 @@ func addExtendedPrefix(path string) string {
150151
copy(buf, prefix)
151152
return syscall.UTF16ToString(buf)
152153
}
154+
155+
// validatePathForCreate checks if a given path is valid for creation on a Windows system.
156+
// It returns true if the path is considered valid, and false otherwise.
157+
// The function performs the following checks:
158+
// 1. If the path is empty, it is considered valid.
159+
// 2. If the path starts with `\\?\` or \??\, it is considered valid without further checks.
160+
// 3. Otherwise, a path ending with a space or . is invalid.
161+
func validatePathForCreate(path string) bool {
162+
// Check if the path is empty.
163+
if len(path) == 0 {
164+
return true
165+
}
166+
// Paths starting with \\?\ should be considered valid without further checks.
167+
if stringslite.HasPrefix(path, `\\?\`) || stringslite.HasPrefix(path, `\??\`) {
168+
return true
169+
}
170+
// Get the base name of the path to check only the last component.
171+
base := filepathlite.Base(path)
172+
// Check if the last character of the base name is a space or period, which is invalid.
173+
switch base[len(base)-1] {
174+
case ' ':
175+
return false
176+
case '.':
177+
return base == "." || base == ".."
178+
default:
179+
return true
180+
}
181+
}

src/os/path_windows_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,56 @@ func BenchmarkAddExtendedPrefix(b *testing.B) {
278278
os.AddExtendedPrefix(veryLong)
279279
}
280280
}
281+
282+
func TestValidatePathForCreate(t *testing.T) {
283+
tests := []struct {
284+
path string
285+
pass bool
286+
}{
287+
{`C:\foo`, true},
288+
{`C:\foo\ `, false},
289+
{`C:\foo\.`, true},
290+
{`C:\foo.`, false},
291+
{`C:\foo\ .`, false},
292+
{`C:\foo \`, false},
293+
{"C:/foo/", true},
294+
{"C:/foo", true},
295+
{"C:/foo/.", true},
296+
{"C:/foo/ .", false},
297+
{".", true},
298+
{"..", true},
299+
{"...", false},
300+
{`\\?\C:\foo`, true},
301+
{`\\?\C:\foo\ `, true},
302+
{`\\?\C:\foo\.`, true},
303+
{`\??\C:\foo`, true},
304+
{`\??\C:\foo\ `, true},
305+
{`\??\C:\foo\.`, true},
306+
{`\\server\share\path`, true},
307+
{"", true},
308+
{" ", false},
309+
{"C:.", true},
310+
{"C: ", false},
311+
{"C:..", true},
312+
{"C:...", false},
313+
{"foo:.", false},
314+
{"C:bar:..", false},
315+
{`\..`, true},
316+
{`\...`, false},
317+
{"/.", true},
318+
{"/..", true},
319+
{"a..", false},
320+
{"aa..", false},
321+
{"a ", false},
322+
{`a\ `, false},
323+
{`a \`, false},
324+
{`.\`, true},
325+
{`..\`, true},
326+
}
327+
328+
for _, tt := range tests {
329+
if os.ValidatePathForCreate(tt.path) != tt.pass {
330+
t.Errorf("validatePathForCreate(%q) = %v, want %v", tt.path, !tt.pass, tt.pass)
331+
}
332+
}
333+
}

0 commit comments

Comments
 (0)