Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os: MkdirAll fails when a parent folder contains trailing spaces #54040

Open
megabild opened this issue Jul 25, 2022 · 19 comments
Open

os: MkdirAll fails when a parent folder contains trailing spaces #54040

megabild opened this issue Jul 25, 2022 · 19 comments
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. OS-Windows
Milestone

Comments

@megabild
Copy link

What version of Go are you using (go version)?

$ go version
go version go1.18.4 windows/amd64

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
set GO111MODULE=on
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\TEST\AppData\Local\go-build
set GOENV=C:\Users\TEST\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=C:\Users\TEST\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\TEST\go;C:\Daten\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.18.4
set GCCGO=gccgo
set GOAMD64=v1
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=C:\Daten\go\src\megabildgo\go.mod
set GOWORK=
set CGO_CFLAGS=-g -O2
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-g -O2
set CGO_FFLAGS=-g -O2
set CGO_LDFLAGS=-g -O2
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -fmessage-length=0 -fdebug-prefix-map=C:\Users\TEST\AppData\Local\Temp\go-build1829370843=/tmp/go-build -gno-record-gcc-switches

What did you do?

err := os.MkdirAll("C:\\temp\\folder \\this one fails", 0644)
if err != nil {
fmt.Println(err)
}

What did you expect to see?

All folders being created

What did you see instead?

There is a folder "folder" without trailing space. The folder "this one fails" is not created.
an os.PathError is thrown:
mkdir C:\temp\folder \this one fails: Das System kann den angegebenen Pfad nicht finden.

@mengzhuo mengzhuo added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Jul 25, 2022
@mengzhuo
Copy link
Contributor

cc @golang/windows

@qmuntal
Copy link
Contributor

qmuntal commented Jul 25, 2022

Windows docs have a special entry for dealing with whitespaces in files and folder names: https://docs.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/file-folder-name-whitespace-characters.

The relevant part is this:

File and Folder names that begin or end with the ASCII Space (0x20) will be saved without these characters. File and Folder names that end with the ASCII Period (0x2E) character will also be saved without this character. All other trailing or leading whitespace characters are retained.

Looking at how MkdirAll is implemented on windows, it seems that we are not removing leading and trailing whitespaces from non-leaf folders. This leads to failure mode:

  1. MkdirAll("C:\temp\folder \this one fails", 0644)
  2. MkdirAll("C:\temp", 0644)
  3. MkdirAll("C:\temp\folder ", 0644) // But Windows created "C:\temp\folder"!!
  4. Mkdir("C:\temp\folder \this one fails", 0644) // Wops, parent folder does not exist

@seankhliao seankhliao changed the title os/path: MkdirAll fails when a parent folder contains trailing spaces os: MkdirAll fails when a parent folder contains trailing spaces Jul 25, 2022
@seankhliao
Copy link
Member

how much normalization should we do vs having the user specify \\?\ when they need weird paths?

@qmuntal
Copy link
Contributor

qmuntal commented Jul 25, 2022

Normalizing this should be trivial, but I would argue against doing so. It would fix the MkdirAll, yet the underlying problem will still be there: the white-spaced path won't exist even though MkdirAll succeed, so any other operation on that path is going to fail.

We either always normalize all paths or we don't, else we will just be moving the failure to a later point, making it worst.

As @seankhliao mentioned, Windows already supports trailing whitespaces when the path is prefixed with \\?\, so the workaround is there.

Additional data point: .Net does not support this either for the sake of interoperability. dotnet/runtime#23292 (comment).

@mattn
Copy link
Member

mattn commented Jul 25, 2022

I am also against normalizing this. If Windows changes its behavior in the future, Go will have to follow the changes.

@megabild
Copy link
Author

Normalizing this should be trivial, but I would argue against doing so. It would fix the MkdirAll, yet the underlying problem will still be there: the white-spaced path won't exist even though MkdirAll succeed, so any other operation on that path is going to fail.

We either always normalize all paths or we don't, else we will just be moving the failure to a later point, making it worst.

As @seankhliao mentioned, Windows already supports trailing whitespaces when the path is prefixed with \\?\, so the workaround is there.

Additional data point: .Net does not support this either for the sake of interoperability. dotnet/runtime#23292 (comment).

Creating the folder with \?\ works with MkdirAll. Can we make this the default behavior on windows? Otherwise the programmer needs to prefix the MkdirAll call if needed. I wonder if anyone even knows about that special \?\ thing.

@qmuntal
Copy link
Contributor

qmuntal commented Jul 25, 2022

I wonder if anyone even knows about that special \\?\ thing.

From Windows docs:

For file I/O, the "\?" prefix to a path string tells the Windows APIs to disable all string parsing and to send the string that follows it straight to the file system.

@bcmills
Copy link
Contributor

bcmills commented Jul 25, 2022

We either always normalize all paths or we don't, else we will just be moving the failure to a later point, making it worse.

I agree that we should not normalize paths here, but could we at least improve the failure mode here?

(Perhaps something in the call stack could notice the leading or trailing space in the intermediate paths and fail with a clearer error message.)

@qmuntal
Copy link
Contributor

qmuntal commented Jul 25, 2022

I agree that we should not normalize paths here, but could we at least improve the failure mode here?

Improving the error message seems like a good idea, but I would try to avoid adding logic to detect leading/trailing whitespaces or periods and instead rely on giving general tips based on windows error codes.

For example, MkdirAll logic ensures that intermediate directories exist by calling CreateDirectory on every non-existing intermediate directory. So, if syscall.CreateDirectory returns ERROR_PATH_NOT_FOUND (see error description in docs), it means that either a concurrent process deleted the directory or that Windows did not honor the folder path in a previous CreateDirectory call.

@seankhliao seankhliao added this to the Unplanned milestone Aug 20, 2022
@gopherbot
Copy link

Change https://go.dev/cl/458136 mentions this issue: os: handle trailing spaces case on MkdirAll

@vladtenlive
Copy link

@bcmills @qmuntal I realized that syscall.ERROR_PATH_NOT_FOUND is Windows OS specific and it will not compile on different platforms. How could I avoid this or wrap it properly?

@ianlancetaylor
Copy link
Contributor

@vladtenlive Use os.IsNotExist(err).

@ianlancetaylor
Copy link
Contributor

Or I guess that we now recommend the equivalent errors.Is(err, fs.ErrNotExist).

@vladtenlive
Copy link

@bcmills
Copy link
Contributor

bcmills commented Jan 3, 2023

I would try to avoid adding logic to detect leading/trailing whitespaces or periods and instead rely on giving general tips based on windows error codes.

if syscall.CreateDirectory returns ERROR_PATH_NOT_FOUND (see error description in docs), it means that either a concurrent process deleted the directory or that Windows did not honor the folder path in a previous CreateDirectory call.

I think that leaves us back where we started: if we can't distinguish between “a concurrent process deleted the directory“ and “Windows did not honor the folder path in a previous CreateDirectory call”, then the existing ERROR_PATH_NOT_FOUND message seems like about the best we can do.

@bcmills
Copy link
Contributor

bcmills commented Jan 3, 2023

Normalizing this should be trivial, but I would argue against doing so. It would fix the MkdirAll, yet the underlying problem will still be there: the white-spaced path won't exist even though MkdirAll succeed, so any other operation on that path is going to fail.

Note that many of the functions in the os package already call the fixLongPath helper function, which converts paths of length ≥ 248 to the equivalent \\?\ form provided that os.canUseLongPaths is also false (see #3358, #41734).

So under certain conditions today, Mkdir (and MkdirAll) will actually succeed in creating the space-containing paths as requested.

In other conditions, Mkdir silently creates a path that differs by a trailing character, and MkdirAll creates a path that differs from some parent directory by a trailing character, then fails. Neither of those behaviors seems like what the user intends, and neither is particularly useful.


I think that, rather than letting the OS do as it likes and possibly fail in ways that are subtle and difficult to predict, we should explicitly choose to either always succeed or always fail for these paths. That is, we should choose either:

a. Change os.Mkdir (and probably also os.OpenFile with O_CREATE as well?) to check path components for non-UNC paths for trailing ASCII spaces and periods, and explicitly reject paths that end in an ASCII space or period.

  • As a side effect, that would cause os.MkdirAll to refuse to create such paths.
  • That would be consistent with https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions, which says in no uncertain terms: “Do not end a file or directory name with a space or a period.”
  • Users who intentionally create such files or directories could switch to using the \\?\ form to manipulate them.
  • Opening such files and/or directories for reading would not be rejected explicitly, but other existing bugs may prevent their use. (For example, os.ReadDir at HEAD seems to fail for such a directory.)

or:

b. Change (and rename) fixLongPath to check for such paths and escape them to their corresponding \\?\ forms.

  • This would allow Go programs to create files and directories with names with trailing space or period characters if the underlying filesystem permits them, which would make the behavior of Go programs on Windows more consistent with their behavior on Unix platforms.
    • However, it would not fix other functions like os.ReadDir that fail for such paths today, so the improved consistency would be limited at best. (Unless we fully open the can of worms and try to fix everything to work with these paths...)
  • It would also not fix the trailing-space problem for relative paths until os: handle relative paths in fixLongPath on Windows #41734 is also addressed.

On balance, I would prefer to make Mkdir and OpenFile reject these paths explicitly.

(CC @neild @zx2c4 for Windows path normalization, the gift that keeps on giving)

@bcmills
Copy link
Contributor

bcmills commented Jan 3, 2023

I would suggest the following (https://go.dev/play/p/T63O30FOM8Y?v=gotip) as a regression test. It passes today on Linux.

// Regression test for https://go.dev/issue/54040:
// Mkdir for a path ending with a trailing ASCII space (or period)
// should either return a non-nil error or create a usable directory.
func TestMkdirTrailingSpace(t *testing.T) {
	parent := t.TempDir()

	dir := filepath.Join(parent, "ends in space ")
	err := os.Mkdir(dir, 0777)
	if err != nil {
		if runtime.GOOS == "windows" && !strings.HasPrefix(dir, `\\?\`) {
			t.Skipf("skipping: Windows does not support paths with trailing spaces")
		}

		t.Fatal(err)
	}

	t.Logf("Mkdir(%q, 0777)", dir)
	t.Cleanup(func() {
		err := os.Remove(dir)
		if err == nil {
			t.Logf("Remove(%q)", dir)
		} else {
			t.Error(err)
		}
	})

	fi, err := os.Lstat(dir)
	if err == nil {
		t.Logf("Lstat(%q).Name() = %q", dir, fi.Name())
	} else {
		t.Error(err)
	}

	fi, err = os.Stat(dir)
	if err == nil {
		t.Logf("Stat(%q).Name() = %q", dir, fi.Name())
	} else {
		t.Error(err)
	}

	ents, err := os.ReadDir(dir)
	if err == nil {
		t.Logf("ReadDir(%q) = %v", dir, ents)
	} else {
		t.Error(err)
	}

	sub := filepath.Join(dir, "sub")
	err = os.Mkdir(sub, 0777)
	if err == nil {
		t.Logf("Mkdir(%q, 0777)", sub)
		err := os.Remove(sub)
		if err == nil {
			t.Logf("Remove(%q)", sub)
		} else {
			t.Error(err)
		}
	} else {
		t.Error(err)
	}
}

@neild
Copy link
Contributor

neild commented Jan 3, 2023

Change os.Mkdir (and probably also os.OpenFile with O_CREATE as well?) to check path components for non-UNC paths for trailing ASCII spaces and periods, and explicitly reject paths that end in an ASCII space or period.

This option sounds reasonable to me. (Although rather than "for non-UNC paths", say "for paths not starting with \\?\--UNC paths are something different.)

@vladtenlive
Copy link

Thank you, @bcmills, for such a great explanation with the possible options.
I will start working on approach a., check it with the regression test you provided, and share the results here for further discussion.

a. Change os.Mkdir (and probably also os.OpenFile with O_CREATE as well?) to check path components for non-UNC paths for trailing ASCII spaces and periods, and explicitly reject paths that end in an ASCII space or period.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. OS-Windows
Projects
None yet
Development

No branches or pull requests

10 participants