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: Windows: Stat fails on special files like 'con' #34900

Closed
lifenjoiner opened this issue Oct 14, 2019 · 9 comments
Closed

os: Windows: Stat fails on special files like 'con' #34900

lifenjoiner opened this issue Oct 14, 2019 · 9 comments
Labels
help wanted NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. OS-Windows
Milestone

Comments

@lifenjoiner
Copy link

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

$ go version
go version go1.13.1 windows/386

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 GOHOSTARCH=386
set GOHOSTOS=windows

What did you do?

try test_Stat-con.go to reproduce the issue

package main

import (
	"os"
)

func main() {
	_, err := os.Stat("con")
	if err != nil {
		panic(err)
	}
}

I have debugged and located that GetFileAttributesEx fails on special files like 'con':

err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
// Not a symlink.
fs := &fileStat{
FileAttributes: fa.FileAttributes,
CreationTime: fa.CreationTime,
LastAccessTime: fa.LastAccessTime,
LastWriteTime: fa.LastWriteTime,
FileSizeHigh: fa.FileSizeHigh,
FileSizeLow: fa.FileSizeLow,
}
if err := fs.saveInfoFromPath(name); err != nil {
return nil, err
}
return fs, nil
}

and only 'nul':

if isWindowsNulName(file.name) {
return &devNullStat, nil
}

if isWindowsNulName(file.name) {
return &devNullStat, nil
}

reserved names
CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9:
https://docs.microsoft.com/zh-cn/windows/win32/fileio/naming-a-file#naming-conventions
CONIN$ and CONOUT$:
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles

ref:
https://github.com/gcc-mirror/gcc/blob/bbcdad768752f5cab6727242515bcd15706acbea/gcc/ada/adaint.c#L1606-L1618

What did you expect to see?

We can access these special Windows files, especially con for printing to stdout.

What did you see instead?

I found sometimes we need to use stdout as a file:
https://github.com/DNSCrypt/dnscrypt-proxy/blob/858957ce91015057a1f6c5a9a24f3ea2beb48537/dnscrypt-proxy/example-dnscrypt-proxy.toml#L339-L341
But con doesn't work.
By doing some research, I located the code here as described.
GetFileAttributesEx and CreateFile of Stat failed on the reserved file name con, and only 'nul' is considered as the special case.

@ianlancetaylor ianlancetaylor changed the title Windows: os.Stat fails on special files like 'con' os: Windows: Stat fails on special files like 'con' Oct 14, 2019
@ianlancetaylor ianlancetaylor added NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. OS-Windows labels Oct 14, 2019
@ianlancetaylor ianlancetaylor added this to the Unplanned milestone Oct 14, 2019
@ianlancetaylor
Copy link
Contributor

Not quite sure what to do here if GetFileAttribuesEx fails on CON. What should os.Stat return?

CC @alexbrainman @mattn

@mattn
Copy link
Member

mattn commented Oct 15, 2019

#include <stdio.h>
#include <sys/stat.h>

int
main(int argc, char* argv[]) {
  FILE* fp = fopen("CON", "r");
  if (fp == NULL) {
	perror("fopen");
	return 1;
  }
  fclose(fp);

  struct stat st;
  int r = stat("CON", &st);
  printf("result of stat: %d\n", r);

  printf("st_dev = %d\n", st.st_dev);
  printf("st_ino = %d\n", st.st_ino);
  printf("st_mode = %x\n", st.st_mode);
  printf("st_nlink = %d\n", st.st_nlink);
  printf("st_uid = %d\n", st.st_uid);
  printf("st_gid = %d\n", st.st_gid);
  printf("st_rdev = %d\n", st.st_rdev);
  printf("st_size = %lu\n", st.st_size);
  return 0;
}
result of stat: 0
st_dev = 2
st_ino = 0
st_mode = 81b6
st_nlink = 1
st_uid = 0
st_gid = 0
st_rdev = 2
st_size = 0

In this C program, stat(2) return 0 with successful. Go also should return nil for err.

@mattn
Copy link
Member

mattn commented Oct 15, 2019

Hopefully this will work.

diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go
index 3e0e0a59ed..a6830c0f6e 100644
--- a/src/os/stat_windows.go
+++ b/src/os/stat_windows.go
@@ -74,6 +74,18 @@ func stat(funcname, name string, createFileAttrs uint32) (FileInfo, error) {
 		}
 		return fs, nil
 	}
+	if err == syscall.ERROR_FILE_NOT_FOUND {
+		if attr, err := syscall.GetFileAttributes(namep); err == nil && attr == syscall.FILE_ATTRIBUTE_ARCHIVE {
+			return &fileStat{
+				name: name,
+				// hopefully this will work for CON, PRN, AUX, CONOUT$ or etc.
+				vol:   0,
+				idxhi: 0,
+				idxlo: 0,
+			}, nil
+		}
+	}
+
 	// GetFileAttributesEx fails with ERROR_SHARING_VIOLATION error for
 	// files, like c:\pagefile.sys. Use FindFirstFile for such files.
 	if err == windows.ERROR_SHARING_VIOLATION {

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/201157 mentions this issue: os: Fix Stat() with special file on Windows

@lifenjoiner
Copy link
Author

lifenjoiner commented Oct 15, 2019 via email

@lifenjoiner
Copy link
Author

@mattn
If your solution works well, maybe we can remove:

if isWindowsNulName(file.name) {
return &devNullStat, nil
}

if isWindowsNulName(file.name) {
return &devNullStat, nil
}

and maybe isWindowsNulName can be cleaned:

go/src/os/file.go

Lines 565 to 581 in a38a917

// isWindowsNulName reports whether name is os.DevNull ('NUL') on Windows.
// True is returned if name is 'NUL' whatever the case.
func isWindowsNulName(name string) bool {
if len(name) != 3 {
return false
}
if name[0] != 'n' && name[0] != 'N' {
return false
}
if name[1] != 'u' && name[1] != 'U' {
return false
}
if name[2] != 'l' && name[2] != 'L' {
return false
}
return true
}

WinAPI CreateDirectory should have covered these reserved names:
func Mkdir(path string, mode uint32) (err error) {
pathp, err := UTF16PtrFromString(path)
if err != nil {
return err
}
return CreateDirectory(pathp, nil)
}

confiremed by

package main

import (
	"os"
)

func main() {
	err := os.Mkdir("con", os.ModePerm)
	if err != nil {
		panic(err)
	}
}

@lifenjoiner
Copy link
Author

append:
on windows, mkdir nul goes silence and returns success, mkdir conin$ and mkdir conin$ have no visible effect and also succeed, while other reserved name trigger errors ...

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/373355 mentions this issue: os: return value for Stat() for files with a reserved name on windows

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/560755 mentions this issue: os: support Stat and LStat for CON device on Windows

ezz-no pushed a commit to ezz-no/go-ezzno that referenced this issue Feb 18, 2024
\\.\con and CON need to be opened with GENERIC_READ access, else
CreateFile will fail with ERROR_INVALID_PARAMETER.

Special-case ERROR_INVALID_PARAMETER in os.[L]Stat so it retries with
GENERIC_READ access.

Fixes golang#34900.

Change-Id: I5010e736d0189c8ada4fc0eca98d71a438c41426
Reviewed-on: https://go-review.googlesource.com/c/go/+/560755
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Reviewed-by: David Chase <drchase@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted 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

4 participants