tkinter: os.dup2() on fds 1/2 breaks every subsequent Tcl interpreter on Windows
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 38/100
Línea de trabajo
Start with Modules/_tkinter.c and the Tcl ticket linked in the issue; trace how Tkapp_New() initializes Tcl and its standard channels, then reproduce the Windows failure with the provided script. The issue proposes installing channels from duplicated handles via Tcl_SetStdChannel(), but notes that this direction has not been validated in C. Done means subsequent Tk() and Tcl() interpreters initialize successfully after fd 1 and 2 are redirected and restored.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Bug report
Bug description:
On Windows, once a Tcl interpreter has built its standard channels, a later os.dup2() onto fds 1 and 2 can make every subsequent tkinter.Tk() or tkinter.Tcl() fail:
_tkinter.TclError: Can't find a usable init.tcl in the following directories:
{C:\Users\...\Python314\tcl\tcl8.6}
C:/Users/.../tcl/tcl8.6/init.tcl: couldn't read file "C:/Users/.../tcl/tcl8.6/init.tcl": no such file or directory
The file is present and readable the whole time. At the instant of the failure Python can os.stat() and open() that same path, and a Tcl interpreter created before the redirection can still source it. The errno is not real.
This is not GUI-specific — tkinter.Tcl() is affected too — and it does not need threads, subprocesses or a window.
Reproducer
Fails 10/10 runs here, with stdout going to a console, a pipe, a file or NUL, for both Tk() and Tcl():
import os, sys, tempfile, tkinter
saved = {fd: os.dup(fd) for fd in (1, 2)}
tmp = {fd: tempfile.TemporaryFile() for fd in (1, 2)}
use_tcl = "--tcl" in sys.argv
def new_interpreter():
if use_tcl:
ip = tkinter.Tcl() # no GUI at all
ip.eval("fconfigure stdout") # make Tcl build its std channels
del ip
else:
tkinter.Tk().destroy()
for i in range(30):
for fd in (1, 2): # redirect fds 1 and 2 ...
os.dup2(tmp[fd].fileno(), fd)
try:
new_interpreter()
except Exception as exc:
for fd in (1, 2):
os.dup2(saved[fd], fd)
print(f"interpreter {i} failed:\n{exc}")
sys.exit(1)
for fd in (1, 2): # ... and put them back
os.dup2(saved[fd], fd)
print("30 interpreters, no failure")
It usually fails at interpreter 1 or 2. Two interpreters and two dup2() calls are enough to trigger it, but that minimal form is sensitive to how handle values happen to be laid out — it fails 18/20 when the real stdout is NUL and 0/20 when it is a pipe. The loop above is stable everywhere, so it is the better reproducer.
What is happening
- Tcl builds its
stdout/stderrchannels on first use out of the valuesGetStdHandle()returns, and keeps them for the life of the process. os.dup2()on fds 1 and 2 goes through the UCRT's_dup2, which for fds 0/1/2 closes the handle currently installed and publishes the new one withSetStdHandle(). The values Tcl cached are now closed handles.- Windows recycles those values immediately. Traced across one redirect/restore cycle (
#fd:handle/type):
start fd1=#1:184/PIPE fd2=#2:188/PIPE stdout=184 stderr=188
after redirect fd1=#1:700/DISK fd2=#2:184/DISK stdout=700 stderr=184
after restore fd1=#1:724/PIPE fd2=#2:700/PIPE stdout=724 stderr=700
Handle 184 is the real stdout pipe at startup and a temporary file one dup2 later. Handle 700 is what Tcl cached as stdout for the first interpreter, and one dup2 later it belongs to fd 2.
- Creating a further Tcl interpreter then fails while reading its own
init.tcl.
Step 1 is the necessary condition, and it is testable on its own. A bare Tcl interpreter that never touches its standard channels never fails; add one fconfigure and it fails like the rest:
60 interpreters per run, same dup2 pattern |
runs failed |
|---|---|
tkinter.Tcl(), standard channels never used |
0/10 |
same, plus fconfigure stdout; fconfigure stderr |
10/10 |
Tk matters only because Tk_Init creates those channels.
The converse holds too. If the handles Tcl caches are private duplicates that nothing else can close, the failure disappears. Done from Python with ctypes — DuplicateHandle() the current fd 1/2 handles, SetStdHandle() the copies, create one interpreter so Tcl caches them, then never close them:
| runs failed | |
|---|---|
| as above | 15/15 |
| identical, but std handles pinned to private duplicates first | 0/15 |
Why this looks like a _tkinter issue
This is Tcl ticket 91c9bc1c45, "Standard channels (stdin, stdout, stderr) on Windows may get broken by dup2()", filed 2018-11-09 against Tcl 8.6.8. It names the same mechanism — TclpGetDefaultStdChannel() in win/tclWinChan.c, broken by _dup2 on fds 0/1/2 — and proposes DuplicateHandle() on the value from GetStdHandle() before handing it to Tcl_MakeFileChannel().
It was closed Invalid on 2026-10-15, with the stated reason that there had been no reply since 2018. The 2018 objection from a Tcl maintainer was that third-party code should call Tcl_SetStdChannel() when it changes the standard handles. In this situation _tkinter is the only component in a position to do that: os.dup2() on fds 0/1/2 is ordinary, supported Python, and the code calling it has no idea Tcl is embedded in the process.
So the equivalent of the upstream patch, on this side, would be for _tkinter to install the standard channels itself from duplicated handles via Tcl_SetStdChannel() — once per process, before Tcl_Init() runs in Tkapp_New() (Modules/_tkinter.c). I have only verified the ctypes equivalent above, not a C patch, so I may well be missing a constraint; I am happy to put a patch together if this direction seems right.
Why it matters in practice
pytest's default --capture=fd does exactly this dup2 pattern around every test. Any Windows test suite that creates a Tcl/Tk interpreter per test hits it:
pytest configuration, 30 tests creating one Tk() each |
runs failed |
|---|---|
--capture=fd (the default) |
16/20 |
-s / --capture=sys / --capture=tee-sys |
0/20 |
one test creating 30 interpreters in a loop, --capture=fd |
0/20 |
That is pytest-dev/pytest#10364, open since 2022. Applying the pin described above from a conftest.py takes that suite from 10/20 runs failing to 0/20.
What I have not established
I have not traced the step from "Tcl is holding a stale standard handle" to the specific couldn't read file ...: no such file or directory. The reported errno is clearly not the real one, and the upstream ticket describes a different surface symptom from the same cause (failing writes, EBADF) — so I assume it is stale rather than meaningful, but I have not proved it.
Two things that did not pan out, in case they save someone time: Tcl does not close standard handles it does not own (a planted sentinel handle survives interpreter create + destroy), and creating a Tk root before the redirection does not help, because the first dup2 closes the original stdout handle too.
CPython versions tested on:
3.14 (3.14.4, Windows installer build, Tcl/Tk 8.6.15). I do not have other versions on this machine, so I have not checked whether main behaves differently — nothing in the mechanism looks version-specific.
Operating systems tested on:
Windows (Windows 11 Pro, build 26200).
- Lenguaje dominante
- Python
- Estrellas
- 78.2k
- Forks
- 36.6k
- Merge medio
- 19 h 57 min
- PR fusionados (30 d)
- 630
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de python/cpython
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
stdlib type-bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
extension-modules stdlib topic-XML type-bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
python/cpython#161807 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
stdlib type-feature
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
python/cpython#161788 · 7 comentarios ·
Los mantenedores suelen responder en 1 día
-
extension-modules topic-IO type-bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
Todos los issues de python/cpython
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
cpp-best-practices/project_validator#267 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Project-N-E-K-O/N.E.K.O#3725 ·
Los mantenedores suelen responder en 1 día
-
[Bug] Sending a Telegram message chain twice duplicates mentionsPosiblemente ocupada @123XieC la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
AstrBotDevs/AstrBot#11125 ·
Los mantenedores suelen responder en 1 día
-
backend bug priority: high
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
nooriegarg/ContribIQ#73 ·
Los mantenedores suelen responder en 1 día
-
bug p3 security
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
amd/gaia#8197 · 2 comentarios ·
Los mantenedores suelen responder en 1 día