Open 3972415a-0eb5-40af-8526-eecd8c293a0a opened 3 years ago
The fallback detection for win_getpass
checks that sys.stdin
is different from sys.__stdin__
. If yes, it assumes that it's incapable of disabling echo, and calls default_getpass
which reads from stdin.
If they are the same object, it assumes it's in a terminal and uses msvcrt
to read characters.
I was not able to find any rationale for this check, it seems to have been introduced, oh wow, in 2001, to fix something IDLE-related.
Anyway, the check is trivially incorrect. It fails when such script is launched from subprocess.Popen(stdin=PIPE)
, where both sys.stdin
and sys.__stdin__
are the same instance of TextIOWrapper
. Same actually happens in MinTTY (Git Bash etc.) which is not a true terminal as far as I was able to find out?
It seems that a better check would be, e.g., sys.stdin.isatty()
, which correctly returns False
both in subprocesses and in MinTTY.
...this is a problem because:
When the check incorrectly infers that it can use msvcrt
while its stdin is a pipe, the calls to putwch
and getwch
are going into the void and the program effectively freezes waiting for input that never comes.
See also: https://stackoverflow.com/questions/49858821/python-getpass-doesnt-work-on-windows-git-bash-mingw64/54046572 https://github.com/ipython/ipython/issues/854
For that matter, in standard Windows Command Prompt sys.stdin
and sys.__stdin__
are also identical, but isatty()
reports True.
I suspect is that the code has drifted and sys.stdin
is always identical to sys.__stdin__
?
When the check incorrectly infers that it can use
msvcrt
while its stdin is a pipe, the calls toputwch
andgetwch
are going into the void and the program effectively freezes waiting for input that never comes.
The C runtime's getwch() and putwch() functions use the console I/O files "CONIN$" and "CONOUT$". If "CONIN$" can't be opened because the process has no console, then getwch() fails and returns U+FFFF. But mintty does have a console, albeit with a hidden window, which gets inherited by bash and child processes. So getwch() ends up waiting for a key to be pressed in an inaccessible, hidden window.
It should suffice to check that sys.stdin isn't None and that sys.stdin.isatty() is True, since it's reasonable to assume that the console has a visible window in this case. Currently this isn't a bulletproof check, however, because the Windows C runtime doesn't implement isatty() correctly. It returns true for any character device, which includes the common case of redirecting standard I/O to the "NUL" device. io.FileIO.isatty() could be updated to return True only if both C isatty() is true and GetConsoleMode() succeeds. This would filter out false positives for non-console character devices, particularly "NUL".
We could also provide a better check in WindowsConsoleIO.isatty, since that should have already handled most of the previous checks (I wouldn't want to do a typecheck in getpass, though). But yeah, this seems like "sys.stdin and sys.stdin.isatty()" is the right condition.
It should be noted that changing win_getpass() to check sys.stdin.isatty() makes it inconsistent with unix_getpass(). The latter tries to open "/dev/tty" regardless of sys.stdin. To be consistent, win_getpass() would be implemented to call fallback_getpass() only if "CONIN$" is inaccessible.
We'd still have the problem that's reported here. A classic console session (not ConPTY) can have no window, or an invisible window, and thus no way for a user to enter text. I don't see any reports that reading from "/dev/tty" makes getpass() wait forever in POSIX, so this situation is probably unique to Windows, or at least it's far more likely in Windows, which may justify introducing an inconsistency.
We could also provide a better check in WindowsConsoleIO.isatty,
For isatty(), my concern is a false positive if the file is the "NUL" device, which should be an io.FileIO object. I suppose checking the file type in io._WindowsConsoleIO.isatty() could be useful in case the file descriptor gets reassigned to a non-console file (e.g. via os.close or os.dup2).
I'd prefer to implement the check in a common _Py_isatty() function that supports the optional errno values EBADF and ENOTTY [1]. For example:
int
_Py_isatty(int fd)
{
DWORD mode;
HANDLE handle = _Py_get_osfhandle_noraise(fd);
if (handle == INVALID_HANDLE_VALUE) {
errno = EBADF;
return 0;
}
switch (GetFileType(handle)) {
case FILE_TYPE_CHAR:
break;
case FILE_TYPE_DISK:
case FILE_TYPE_PIPE:
errno = ENOTTY;
return 0;
default:
errno = EBADF;
return 0;
}
if (!GetConsoleMode(handle, &mode) &&
GetLastError() != ERROR_ACCESS_DENIED) {
errno = ENOTTY;
return 0;
}
return 1;
}
[1] https://pubs.opengroup.org/onlinepubs/9699919799/functions/isatty.html
I suppose checking the file type in io._WindowsConsoleIO.isatty() could be useful in case the file descriptor gets reassigned to a non-console file (e.g. via os.close or os.dup2).
Pretty sure we currently don't support these anyway. WindowsConsoleIO doesn't actually use the standard file descriptors for stdin/out/err - it creates new ones based on the real handles just in case a caller requests them. In my opinion, correctness of "write to console" outweighs correctness of "manually close the console FD and wait for a new one with the same number to be opened" ;)
If we have WindowsConsoleIO as the type, it shouldn't be possible for it to refer to any kind of file other than the console or it would be a FileIO. If that's no longer true, we should fix that first. WindowsConsoleIO.isatty ought to be "return True".
WindowsConsoleIO doesn't actually use the standard file descriptors for stdin/out/err
To resolve bpo-30555, _WindowsConsoleIO was changed to eliminate self->handle and get the underlying handle dynamically via _get_osfhandle(). It's thus susceptible to close() and dup2() calls in Python or a C library. This consequence was noted. The alternative was to duplicate the OS handle that self->fd wraps, isolating it from self->handle. But then self->fd could get closed, or duped to another file. Either way there are file and file-type sync problems. It's not going to work in the seamless way that people are used to in POSIX, which only uses a single io.FileIO type. I think a better resolution will be possible in the context of your side project, if you're still working on it...
The rationale for the __stdin and stdin check is this: When python starts, both are usually set an io.TextIOWrapper wrapping a system console. (At least on Windows, both may instead be None instead.) __stdin should never be altered. Getpass.getpass knows how to turn off echo on a system terminal. So if stdin is not None and is not __stdin__, stdin is very likely not a system terminal, and if so, getpass does not know to turn off echo, if indeed this is possible.
As far as I know, the tk and hence tkinter text widget do not come with a option to not display key presses that are stored in the widget. An application would have to intercept keypresses and store them elsewhere. But this is quite different issue.
This issue is not about echo, but about interactivity. Testing that with isatty is *NOT* a substitute for testing whether an interactive device is the system terminal or not. (IDLE's stdio connected to Shell passes isatty.) Any new test would have to added without deleting the current test.
The sys.stdin is not sys.__stdin__
check is not relevant information. We need to know whether msvcrt.getwch() works and that the user should be able to type the password in the console window. sys.stdin could be a file object for the NUL device, a pipe, or a file.
Also, this check has never been implemented in POSIX. If I run python -m idlelib
in Linux, getpass() still reads the password from the terminal instead of sys.stdin.
IDLE's stdio connected to Shell passes isatty
IOBase.isatty() is more abstract than I expected, since it can be true for any stream that's "interactive". While FileIO.isatty() and os.isatty() are definitely wrong in Windows (i.e. they should not be true for NUL or a serial/parallel port), I see now that fixing isatty() doesn't solve the problem.
We need to directly check whether sys.stdin is a console. If so, we can reasonably assume that there's a visible, accessible console window. The check would be something like:
try:
_winapi.GetConsoleMode(msvcrt.get_osfhandle(sys.stdin.fileno()))
except (AttributeError, OSError):
return fallback_getpass(prompt, stream)
_winapi.GetConsoleMode() would need to be implemented.
This is inconsistent with Unix since it's not trying to open "CONIN$". But relying on the latter is problematic in Windows since it succeeds in any process that's attached to a console session, even if the console window is hidden or never created (i.e. CREATE_NO_WINDOW).
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields: ```python assignee = None closed_at = None created_at =
labels = ['type-bug', '3.9', '3.10', '3.11', 'library', 'OS-windows']
title = 'getpass.getpass on Windows fallback detection is incomplete'
updated_at =
user = 'https://github.com/matejcik'
```
bugs.python.org fields:
```python
activity =
actor = 'eryksun'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Library (Lib)', 'Windows']
creation =
creator = 'matejcik'
dependencies = []
files = []
hgrepos = []
issue_num = 44762
keywords = []
message_count = 11.0
messages = ['398370', '398372', '398374', '398399', '398431', '398509', '398510', '398511', '398519', '398962', '399004']
nosy_count = 7.0
nosy_names = ['terry.reedy', 'paul.moore', 'tim.golden', 'matejcik', 'zach.ware', 'eryksun', 'steve.dower']
pr_nums = []
priority = 'normal'
resolution = None
stage = 'needs patch'
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue44762'
versions = ['Python 3.9', 'Python 3.10', 'Python 3.11']
```