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

x/term: "Access is denied." error with ReadPassword on Windows #46164

Closed
codesoap opened this issue May 13, 2021 · 27 comments
Closed

x/term: "Access is denied." error with ReadPassword on Windows #46164

codesoap opened this issue May 13, 2021 · 27 comments
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. OS-Windows
Milestone

Comments

@codesoap
Copy link
Contributor

codesoap commented May 13, 2021

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

$ go version
go version go1.16.2 openbsd/amd64

Does this issue reproduce with the latest release?

It reproduces with the latest version of x/term (v0.0.0-20210503060354-a79de5458b56).

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

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/fooman/.cache/go-build"
GOENV="/home/fooman/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="openbsd"
GOINSECURE=""
GOMODCACHE="/home/fooman/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="openbsd"
GOPATH="/home/fooman/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/openbsd_amd64"
GOVCS=""
GOVERSION="go1.16.2"
GCCGO="gccgo"
AR="ar"
CC="cc"
CXX="c++"
CGO_ENABLED="1"
GOMOD="/home/fooman/tmp_1m/readpw_win/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3256633254=/tmp/go-build -gno-record-gcc-switches"

I'm cross compiling from OpenBSD for Windows.

What did you do?

I ran the following program on the Windows 10 operating system:

package main

import (
	"fmt"
	"golang.org/x/term"
	"os"
)

func main() {
	tty, err := os.Open("CON:")
	if err != nil {
		panic(err)
	}
	defer tty.Close()
	pw, err := term.ReadPassword(int(tty.Fd()))
	if err != nil {
		panic(err)
	}
	fmt.Println(string(pw))
}

What did you expect to see?

I expected the program to prompt me for password input and to then print the entered password.

What did you see instead?

When running the program (either as normal user or as administator) I get this output:

panic: Access is denied.

goroutine 1 [running]:
main.main()
        /home/fooman/tmp_1m/readpw_win/readpw_win.go:17 +0x197

@gopherbot gopherbot added this to the Unreleased milestone May 13, 2021
@codesoap codesoap changed the title x/term: "Access is denied." with the ReadPassword function on Windows x/term: "Access is denied." error with ReadPassword on Windows May 13, 2021
@heschi heschi added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label May 14, 2021
@heschi
Copy link
Contributor

heschi commented May 14, 2021

cc @tklauser @ianlancetaylor

@networkimprov
Copy link

networkimprov commented May 17, 2021

cc @alexbrainman @mattn @zx2c4

@gopherbot add OS-Windows

@alexbrainman
Copy link
Member

@codesoap your version of the code is not expected to work.

Try using https://pkg.go.dev/golang.org/x/term#Terminal.ReadPassword on Windows.

Alex

@codesoap
Copy link
Contributor Author

@alexbrainman Thanks for your response. I now tried using https://pkg.go.dev/golang.org/x/term#Terminal.ReadPassword:

package main

import (
	"fmt"
	"golang.org/x/term"
	"os"
)

func main() {
	fmt.Println(getPw())
}

func getPw() string {
	tty, err := os.Open("CON:")
	if err != nil {
		panic(err)
	}
	defer tty.Close()
	oldState, err := term.MakeRaw(int(tty.Fd()))
	if err != nil {
		panic(err)
	}
	defer term.Restore(int(tty.Fd()), oldState)
	terminal := term.NewTerminal(tty, "")
	pw, err := terminal.ReadPassword("")
	if err != nil {
		panic(err)
	}
	return string(pw)
}

This time using the MakeRaw function failed (same result for normal user and administrator):

panic: Access is denied.

goroutine 1 [running]:
main.getPw(0x0, 0x0)
        /home/fooman/tmp_1m/readpw_win/readpw_win.go:21 +0x2e5
main.main()
        /home/fooman/tmp_1m/readpw_win/readpw_win.go:10 +0x29

If I leave out the call of MakeRaw, the program does not panic, but the input is echoed while typing.

@alexbrainman
Copy link
Member

@codesoap sorry for confusing you. It has been many years since I looked at this code.

Here is your original version with small mod:

package main

import (
        "fmt"
        "golang.org/x/term"
        "os"
)

func main() {
        pw, err := term.ReadPassword(int(os.Stdin.Fd()))
        if err != nil {
                panic(err)
        }
        fmt.Println(string(pw))
}

Here is this program in action:

Z:\>test
hello world

Z:\>

I typed hello world and then pressed enter key as it runs.

I hope it helps.

Alex

@codesoap
Copy link
Contributor Author

codesoap commented May 19, 2021

I probably didn't describe my problem in enough detail. I'm intentionally not using os.Stdin, because in my case standard input is not the terminal, but a pipe. To be more specific: I'm talking about https://github.com/FiloSottile/age, which can take encrypted data via a pipe/standard input and will then prompt the user for a password via the terminal (which is not standard input).

Using /dev/tty on UNIX-like systems works flawlessly in this scenario, but using CON: on Windows does not. It is, hovewer, entirely possible, that I misunderstood the purpose of the CON: device on Windows.

@alexbrainman
Copy link
Member

Using /dev/tty on UNIX-like systems works flawlessly in this scenario, but using CON: on Windows does not. It is, hovewer, entirely possible, that I misunderstood the purpose of the CON: device on Windows.

I am not familiar with https://github.com/FiloSottile/age package. Can you provide a small piece of code that works on UNIX and does not work on Windows? Hopefully this will help me understand what you are trying to achieve.

Thank you.

Alex

@codesoap
Copy link
Contributor Author

Thanks a lot for keeping at it! I hope this program clarifies what I'm trying to achieve. It works on UNIX-like systems, but does not work on Windows after substituting /dev/tty for CON::

package main

import (
	"fmt"
	"golang.org/x/term"
	"io"
	"os"
)

func main() {
	tty, err := os.Open("/dev/tty")
	if err != nil {
		panic(err)
	}
	defer tty.Close()
	fmt.Print("Enter password: ")
	pw, err := term.ReadPassword(int(tty.Fd()))
	if err != nil {
		panic(err)
	}
	fmt.Println("")
	decryptStdin(string(pw))
}

// decryptStdin is only a dummy, which prints what was received via
// standard input. In the real world it would use pw to decrypt the
// standard input before printing it.
func decryptStdin(pw string) {
	fmt.Println("Decrypted message:")
	io.Copy(os.Stdout, os.Stdin)
}

This is how I can use the program on a UNIX-like system; I am prompted to enter the password interactively:

$ echo foo | ./readpw
Enter password:
Decrypted message:
foo

@alexbrainman
Copy link
Member

It works on UNIX-like systems, but does not work on Windows after substituting /dev/tty for CON::

I tried it too, I get Access is denied. just like you did. The error is from SetConsoleMode Windows API. os.Open uses CreateFile Windows API to open the file. Perhaps some CreateFile parameters are set wrong when os.Open is called.

But regardless, you are using 3 different devices in your program: /dev/tty, stdin and stdout. You won't be able to do that on Windows. On Windows you can read and write from console device - that is it. There is only one console input device. If you use SetConsoleMode to adjust console parameters (that is what ReadPassword does), then your console parameters change for any IO.

Alex

@networkimprov
Copy link

@codesoap does this help?
https://docs.microsoft.com/en-us/windows/console/console-handles

You might also want to raise your Q on StackOverflow or golang-nuts for more ideas...

@codesoap
Copy link
Contributor Author

@networkimprov Thanks for the input, but I'm not sure if this is relevant to my problem, since I have no problem with stdin (term.ReadPassword works flawlessly with stdin on Windows). However, I'm also utterly unfamiliar with Windows and it's APIs, so maybe I just don't see how the linked article could help me...

@alexbrainman:

But regardless, you are using 3 different devices in your program: /dev/tty, stdin and stdout. You won't be able to do that on Windows.

I'm not sure if I understood you correctly, but using 3 different devices in the same program definitely works on Windows. If I modify my last provided program to use fmt.Fscanln instead of term.ReadPassword it works as expected:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	tty, err := os.Open("CON:")
	if err != nil {
		panic(err)
	}
	defer tty.Close()
	fmt.Print("Enter password: ")
	var pw string
	_, err = fmt.Fscanln(tty, &pw)
	if err != nil {
		panic(err)
	}
	decryptStdin(pw)
}

// decryptStdin is only a dummy, which prints what was received via
// standard input. In the real world it would use pw to decrypt the
// standard input before printing it.
func decryptStdin(pw string) {
	fmt.Println("Decrypted message:")
	io.Copy(os.Stdout, os.Stdin)
}

This is what I get (on Windows):

> echo foo | readpw.exe
Enter password: bar
Decrypted message:
foo

@networkimprov
Copy link

The "access denied" seems like a Go bug. But you may need to use x/sys/windows to call WinAPIs to work around it.

Ask on StackOverflow, and if that doesn't help, the WinAPI docs are the definitive resource.

cc @mattn

@mattn
Copy link
Member

mattn commented May 30, 2021

Document for CreateFile says:


The following table shows various settings of dwDesiredAccess and lpFileName.

lpFileName dwDesiredAccess Result
"CON" GENERIC_READ Opens console for input.
"CON" GENERIC_WRITE Opens console for output.
"CON" GENERIC_READ | GENERIC_WRITE Causes CreateFile to fail; GetLastError returns ERROR_FILE_NOT_FOUND.

GetConsoleMode with CON: require GENERIC_READ. But SetConsoleMode with CON: require GENERIC_WRITE. This should be particular behavior of CON:. So, MakeRaw always fail for CON:. IMO, you should not use CON: for this use-case.

@codesoap
Copy link
Contributor Author

Thanks for looking into it as well, @mattn ! I'm afraid I don't understand what you wrote very well, because I'm not familiar with Windows' API. I'll take away that what I'm trying to do will not work with term.ReadPassword.

I'm not sure if this issue should be closed or left open, so I'll leave it up to someone more knowledgeable than me to close it if appropriate.

@alexbrainman
Copy link
Member

@codesoap here is version of your program that works:

package main

import (
	"fmt"
	"golang.org/x/term"
	"io"
	"os"
	"syscall"
)

func main() {
	path, err := syscall.UTF16PtrFromString("CONIN$")
	if err != nil {
		panic(err)
	}
	h, err := syscall.CreateFile(path, syscall.GENERIC_READ|syscall.GENERIC_WRITE, syscall.FILE_SHARE_READ, nil, syscall.OPEN_EXISTING, 0, 0)
	if err != nil {
		panic(err)
	}
	defer syscall.CloseHandle(h)

	fmt.Print("Enter password: ")
	pw, err := term.ReadPassword(int(h))
	if err != nil {
		panic(err)
	}

	fmt.Println("")

	decryptStdin(string(pw))
}

// decryptStdin is only a dummy, which prints what was received via
// standard input. In the real world it would use pw to decrypt the
// standard input before printing it.
func decryptStdin(pw string) {
	fmt.Println("Decrypted message:")
	io.Copy(os.Stdout, os.Stdin)
}

I copied part of that code from

https://stackoverflow.com/questions/377152/what-does-createfileconin-do

Alex

@codesoap
Copy link
Contributor Author

Wow, thanks for all your effort, @alexbrainman ! The program you posted does exactly what I was looking for. The only thing I'll change is that I'll use golang.org/x/sys/windows instead of syscall, since the documentation says this package has been deprecated.

As I said before, I can't really gauge whether this issue still has value or should be closed, so I'll leave for someone else to close or pick up.

@networkimprov
Copy link

What happens if you use Open("CONIN$") in your original program?

@alexbrainman
Copy link
Member

The only thing I'll change is that I'll use golang.org/x/sys/windows instead of syscall, since the documentation says this package has been deprecated.

No worries.

As I said before, I can't really gauge whether this issue still has value or should be closed, so I'll leave for someone else to close or pick up.

So the problem here appears that os.Open does not work with files like CONIN$, if you want to use returned file handle to call GetConsoleMode or SetConsoleMode Windows API.

I think this scenario is too specific. And we easily solved the problem by calling Windows API directly.

I think this is good enough. So closing the issue. Feel free to re-open if you disagree.

Alex

@codesoap
Copy link
Contributor Author

codesoap commented Jun 1, 2021

What happens if you use Open("CONIN$") in your original program?

@networkimprov Unfortunately this still gives the same "Access is denied." error.

@networkimprov
Copy link

Alex, how about detecting "CONIN$" in os.Open and setting the flags accordingly?

@codesoap
Copy link
Contributor Author

codesoap commented Jun 1, 2021

@networkimprov @alexbrainman

@magical Found out something quite curious at FiloSottile/age#274 (comment) : Apparently it will work if CONIN$ is opened with write permissions. This works:

package main

import (
	"fmt"
	"golang.org/x/term"
	"os"
)

func main() {
	tty, err := os.OpenFile("CONIN$", os.O_RDWR, 0777)
	if err != nil {
		panic(err)
	}
	defer tty.Close()
	pw, err := term.ReadPassword(int(tty.Fd()))
	if err != nil {
		panic(err)
	}
	fmt.Println(string(pw))
}

The same does not work with CON:, since it will produce this:

panic: open CON:: The parameter is incorrect.

goroutine 1 [running]:
main.main()
        /tmp/main.go:12 +0x198

Using os.OpenFile("CONIN$", os.O_RDONLY, 0777) instead of os.OpenFile("CONIN$", os.O_RDWR, 0777) will produce the "Access is denied." error.

Could this be a bug in the Windows API? It doesn't make sense to me that CONIN$ (which is explicitly only to be used for reading) must be opened with os.O_RDWR.

@networkimprov
Copy link

Assuming Alex is not inclined to change the os.Open flags for CONIN$, could you submit a patch to the os.Open/OpenFile docs re CON: & CONIN$?

@magical
Copy link
Contributor

magical commented Jun 1, 2021

@networkimprov I think an example in x/term would be more appropriate.

@alexbrainman
Copy link
Member

Alex, how about detecting "CONIN$" in os.Open and setting the flags accordingly?

Why bother? This happened to me first time in my life. I prefer just using CreateFile Windows API - this way I can do exactly what MS documentation requires.

Could this be a bug in the Windows API? It doesn't make sense to me that CONIN$ (which is explicitly only to be used for reading) must be opened with os.O_RDWR.

No it is not bug in Windows. os.OpenFile uses Windows CreateFile API. The implementation maps os.OpenFile parameters into CreateFile parameters. This mapping was implemented mainly for real files. Console files weren't considered while implementing this mapping. If os.OpenFile works for you, consider yourself lucky. I prefer to use CreateFile as per MS documentation.

Alex

@codesoap
Copy link
Contributor Author

codesoap commented Jun 5, 2021

No it is not bug in Windows. os.OpenFile uses Windows CreateFile API. The implementation maps os.OpenFile parameters into CreateFile parameters. This mapping was implemented mainly for real files. Console files weren't considered while implementing this mapping.

@alexbrainman Thanks for further explaining this. I guess you are referring to https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles. It seems like this line is relevant here:

GENERIC_READ | GENERIC_WRITE is preferred, but either one can limit access.

It is not explained how access is limited when using just GENERIC_READ on CONIN$, but it seems like ReadPassword is doing something that requires GENERIC_WRITE. By taking code from https://github.com/golang/term/blob/a79de5458b56c188f4fc267a58014ac25fec956a/term_windows.go#L23 I think I was able to pinpoint the problematic syscall; this example causes the Access is denied error:

package main

import (
	"golang.org/x/sys/windows"
)

func main() {
	path, err := windows.UTF16PtrFromString("CONIN$")
	if err != nil {
		panic(err)
	}
	tty, err := windows.CreateFile(path, windows.GENERIC_READ, windows.FILE_SHARE_READ, nil, windows.OPEN_EXISTING, 0, 0)
	if err != nil {
		panic(err)
	}
	defer windows.CloseHandle(tty)

	var st uint32
	if err := windows.GetConsoleMode(tty, &st); err != nil {
		panic(err)
	}
	raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
	if err := windows.SetConsoleMode(tty, raw); err != nil {
		panic(err)
	}
}
> demo.exe
panic: Access is denied.

goroutine 1 [running]:
main.main()
        /tmp/main.go:24 +0x1a6

So the error is thrown by windows.SetConsoleMode. According to the documentation of SetConsoleMode, however, only the GENERIC_READ flag should be necessary. Can you tell if I'm still misunderstanding something, @alexbrainman?

@alexbrainman
Copy link
Member

I guess you are referring to https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles.

I am not referring to anything. But I did look at

https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles

and

https://docs.microsoft.com/en-us/windows/console/setconsolemode

and

https://docs.microsoft.com/en-us/windows/console/console-buffer-security-and-access-rights

Perhaps other documentation.

It is not explained how access is limited when using just GENERIC_READ on CONIN$,

I agree with you. I could not find explanation of why this fails.

But then I found C code on Stackoverflow that worked for me (see link at #46164 (comment) ). So I stopped trying to question Microsoft documentation.

So the error is thrown by windows.SetConsoleMode.

I agree. I knew that windows.SetConsoleMode was the call that failed from the begining. See #46164 (comment).

however, only the GENERIC_READ flag should be necessary. Can you tell if I'm still misunderstanding something, @alexbrainman?

Like I said above, I stoped wandering about this the moment I found a solution. CreateFile API have 7 parameters. Perhaps other parameters are important too. You should ask Microsoft or someone else who knows why your code does not work.

Sorry I could not be more helpful.

Alex

@codesoap
Copy link
Contributor Author

codesoap commented Jun 10, 2021

Thanks for your response. Since we have seemingly reached the limit of what we can do with the available documentation, I'll accept the behavior of the Windows API for now. If I find the time, I may try to put together a C++ demo of the problem later and ask Microsoft about it.

@golang golang locked and limited conversation to collaborators Jun 10, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge 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

7 participants