Skip to content

Commit 8471b55

Browse files
committed
os: check for valid Windows path when creating files
Checks for a valid Windows path by ensuring the path doesn't trailing spaces or periods. Fixes golang#54040. Cq-Include-Trybots: luci.golang.try:gotip-windows-arm64 Change-Id: I266f79963c821f8cc474097d3e57c5645ad996fc
1 parent 6853d89 commit 8471b55

7 files changed

+212
-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}
@@ -256,6 +272,9 @@ func tempDir() string {
256272
// Link creates newname as a hard link to the oldname file.
257273
// If there is an error, it will be of type *LinkError.
258274
func Link(oldname, newname string) error {
275+
if !validatePathForCreate(newname) {
276+
return &LinkError{"link", oldname, newname, errInvalidPath}
277+
}
259278
n, err := syscall.UTF16PtrFromString(fixLongPath(newname))
260279
if err != nil {
261280
return &LinkError{"link", oldname, newname, err}
@@ -276,6 +295,9 @@ func Link(oldname, newname string) error {
276295
// if oldname is later created as a directory the symlink will not work.
277296
// If there is an error, it will be of type *LinkError.
278297
func Symlink(oldname, newname string) error {
298+
if !validatePathForCreate(newname) {
299+
return &LinkError{"symlink", oldname, newname, errInvalidPath}
300+
}
279301
// '/' does not work in link's content
280302
oldname = filepathlite.FromSlash(oldname)
281303

src/os/os_windows_test.go

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

src/os/path_windows.go

Lines changed: 43 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,45 @@ func addExtendedPrefix(path string) string {
150151
copy(buf, prefix)
151152
return syscall.UTF16ToString(buf)
152153
}
154+
155+
func validatePathForCreate(path string) bool {
156+
// Check if the path is empty.
157+
if len(path) == 0 {
158+
return true
159+
}
160+
// Paths starting with \\?\ should be considered valid without further checks.
161+
if stringslite.HasPrefix(path, `\\?\`) || stringslite.HasPrefix(path, `\\?\UNC\`) {
162+
return true
163+
}
164+
// Get the base name of the path to check only the last component.
165+
base := filepathlite.Base(path)
166+
// Check if the last character of the base name is a space or period, which is invalid.
167+
lastChar := base[len(base)-1]
168+
if lastChar == ' ' || lastChar == '.' {
169+
// If the last character is a period get the previous character.
170+
if lastChar == '.' {
171+
// If the path is only a period, it is valid.
172+
if len(path) == 1 {
173+
return true
174+
}
175+
i := len(path) - 1
176+
for ; i >= len(path)-3 && i > 0; i-- {
177+
if path[i] == '/' || path[i] == '\\' || path[i] == ':' {
178+
break
179+
}
180+
}
181+
v := path[i:]
182+
switch v {
183+
case ".", "/.", "\\.", "..", "/..", "\\..":
184+
return true
185+
case ":.", ":..":
186+
// Colon must be the second char e.g C:. or C:..
187+
if path[1] == ':' && (len(path) == 3 || len(path) == 4) {
188+
return true
189+
}
190+
}
191+
}
192+
return false
193+
}
194+
return true
195+
}

src/os/path_windows_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,51 @@ 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+
{`\\server\share\path`, true},
303+
{"", true},
304+
{"C:.", true},
305+
{"C: ", false},
306+
{"C:..", true},
307+
{"C:...", false},
308+
{"foo:.", false},
309+
{"C:bar:..", false},
310+
{`\..`, true},
311+
{`\...`, false},
312+
{"/.", true},
313+
{"/..", true},
314+
{"a..", false},
315+
{"aa..", false},
316+
{"a ", false},
317+
{`a\ `, false},
318+
{`a \`, false},
319+
{`.\`, false},
320+
{`..\`, false},
321+
}
322+
323+
for _, tt := range tests {
324+
if os.ValidatePathForCreate(tt.path) != tt.pass {
325+
t.Errorf("validatePathForCreate(%q) = %v, want %v", tt.path, !tt.pass, tt.pass)
326+
}
327+
}
328+
}

0 commit comments

Comments
 (0)