Skip to content

Commit

Permalink
os: support Stat and LStat for CON device on Windows
Browse files Browse the repository at this point in the history
\\.\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 <[email protected]>
Reviewed-by: Bryan Mills <[email protected]>
Reviewed-by: David Chase <[email protected]>
  • Loading branch information
qmuntal authored and ezz-no committed Feb 17, 2024
1 parent abeedc2 commit d0b67bb
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 17 deletions.
75 changes: 59 additions & 16 deletions src/os/stat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,35 @@ import (
"io/fs"
"os"
"path/filepath"
"runtime"
"testing"
)

type testStatAndLstatParams struct {
isLink bool
statCheck func(*testing.T, string, fs.FileInfo)
lstatCheck func(*testing.T, string, fs.FileInfo)
}

// testStatAndLstat verifies that all os.Stat, os.Lstat os.File.Stat and os.Readdir work.
func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCheck func(*testing.T, string, fs.FileInfo)) {
func testStatAndLstat(t *testing.T, path string, params testStatAndLstatParams) {
// test os.Stat
sfi, err := os.Stat(path)
if err != nil {
t.Error(err)
return
}
statCheck(t, path, sfi)
params.statCheck(t, path, sfi)

// test os.Lstat
lsfi, err := os.Lstat(path)
if err != nil {
t.Error(err)
return
}
lstatCheck(t, path, lsfi)
params.lstatCheck(t, path, lsfi)

if isLink {
if params.isLink {
if os.SameFile(sfi, lsfi) {
t.Errorf("stat and lstat of %q should not be the same", path)
}
Expand All @@ -53,13 +60,13 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh
t.Error(err)
return
}
statCheck(t, path, sfi2)
params.statCheck(t, path, sfi2)

if !os.SameFile(sfi, sfi2) {
t.Errorf("stat of open %q file and stat of %q should be the same", path, path)
}

if isLink {
if params.isLink {
if os.SameFile(sfi2, lsfi) {
t.Errorf("stat of opened %q file and lstat of %q should not be the same", path, path)
}
Expand All @@ -69,12 +76,13 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh
}
}

// test fs.FileInfo returned by os.Readdir
if len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) {
// skip os.Readdir test of directories with slash at the end
parentdir, base := filepath.Split(path)
if parentdir == "" || base == "" {
// skip os.Readdir test of files without directory or file name component,
// such as directories with slash at the end or Windows device names.
return
}
parentdir := filepath.Dir(path)

parent, err := os.Open(parentdir)
if err != nil {
t.Error(err)
Expand All @@ -88,7 +96,6 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh
return
}
var lsfi2 fs.FileInfo
base := filepath.Base(path)
for _, fi2 := range fis {
if fi2.Name() == base {
lsfi2 = fi2
Expand All @@ -99,7 +106,7 @@ func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCh
t.Errorf("failed to find %q in its parent", path)
return
}
lstatCheck(t, path, lsfi2)
params.lstatCheck(t, path, lsfi2)

if !os.SameFile(lsfi, lsfi2) {
t.Errorf("lstat of %q file in %q directory and %q should be the same", lsfi2.Name(), parentdir, path)
Expand Down Expand Up @@ -140,19 +147,34 @@ func testIsFile(t *testing.T, path string, fi fs.FileInfo) {
}

func testDirStats(t *testing.T, path string) {
testStatAndLstat(t, path, false, testIsDir, testIsDir)
params := testStatAndLstatParams{
isLink: false,
statCheck: testIsDir,
lstatCheck: testIsDir,
}
testStatAndLstat(t, path, params)
}

func testFileStats(t *testing.T, path string) {
testStatAndLstat(t, path, false, testIsFile, testIsFile)
params := testStatAndLstatParams{
isLink: false,
statCheck: testIsFile,
lstatCheck: testIsFile,
}
testStatAndLstat(t, path, params)
}

func testSymlinkStats(t *testing.T, path string, isdir bool) {
params := testStatAndLstatParams{
isLink: true,
lstatCheck: testIsSymlink,
}
if isdir {
testStatAndLstat(t, path, true, testIsDir, testIsSymlink)
params.statCheck = testIsDir
} else {
testStatAndLstat(t, path, true, testIsFile, testIsSymlink)
params.statCheck = testIsFile
}
testStatAndLstat(t, path, params)
}

func testSymlinkSameFile(t *testing.T, path, link string) {
Expand Down Expand Up @@ -294,3 +316,24 @@ func TestSymlinkWithTrailingSlash(t *testing.T) {
t.Errorf("os.Stat(%q) and os.Stat(%q) are not the same file", dir, dirlinkWithSlash)
}
}

func TestStatConsole(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("skipping on non-Windows")
}
t.Parallel()
consoleNames := []string{
"CONIN$",
"CONOUT$",
"CON",
}
for _, name := range consoleNames {
params := testStatAndLstatParams{
isLink: false,
statCheck: testIsFile,
lstatCheck: testIsFile,
}
testStatAndLstat(t, name, params)
testStatAndLstat(t, `\\.\`+name, params)
}
}
11 changes: 10 additions & 1 deletion src/os/stat_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,16 @@ func stat(funcname, name string, followSurrogates bool) (FileInfo, error) {
// save information about the link target.
// Set FILE_FLAG_BACKUP_SEMANTICS so that CreateFile will create the handle
// even if name refers to a directory.
h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT, 0)
var flags uint32 = syscall.FILE_FLAG_BACKUP_SEMANTICS | syscall.FILE_FLAG_OPEN_REPARSE_POINT
h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, flags, 0)

if err == windows.ERROR_INVALID_PARAMETER {
// Console handles, like "\\.\con", require generic read access. See
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#consoles.
// We haven't set it previously because it is normally not required
// to read attributes and some files may not allow it.
h, err = syscall.CreateFile(namep, syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING, flags, 0)
}
if err != nil {
// Since CreateFile failed, we can't determine whether name refers to a
// name surrogate, or some other kind of reparse point. Since we can't return a
Expand Down

0 comments on commit d0b67bb

Please sign in to comment.