fix: a refused write also marks a closed window

A window can still answer tcgetattr while refusing writes. The check now
tries both, as the audit asked, and takes the terminal itself so a stand-in
without a descriptor is never read.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 22:29:00 +02:00
1 parent 19c57b3d68
commit c53715d7b9
2 files changed
+21 -14

No files matched your search

+12 -10
View File
@@ -12969,7 +12969,7 @@ def run_tui(args: argparse.Namespace) -> int:
# The result is printed in the terminal on the way out.
state.exit_code = state.code if state.code is not None else 1
except Exception as exc: # noqa: BLE001
if flags["term"] or _closed_terminal(exc, terminal.fd):
if flags["term"] or _closed_terminal(exc, terminal):
# The window closed under a screen that was changing, or the
# SIGCONT that comes with its SIGHUP put the screen back: the
# write failed before the signal was read. That is its exit.
@@ -13033,28 +13033,30 @@ def _tui_startup(args: argparse.Namespace) -> tuple[TuiState, list[tuple]] | Non
_CLOSED_TERMINAL = frozenset({errno.EIO, errno.EPIPE, errno.ENXIO})
def _closed_terminal(exc: BaseException, fd: int | None = None) -> bool:
def _closed_terminal(exc: BaseException, terminal: _TuiTerminal | None = None) -> bool:
"""Whether exc is the terminal gone: an OSError, or a termios.error,
whose first argument is the errno, answering EIO, EPIPE or ENXIO, and,
given the terminal's fd, that terminal no longer answering. A failing
SD card answers EIO too, and that read error is kept in a crash log."""
given the terminal, that terminal no longer answering. A failing SD
card answers EIO too, and that read error is kept in a crash log."""
if isinstance(exc, OSError):
closed = exc.errno in _CLOSED_TERMINAL
elif type(exc).__name__ == "error" and type(exc).__module__ == "termios":
closed = bool(exc.args) and exc.args[0] in _CLOSED_TERMINAL
else:
closed = False
return closed and (fd is None or _terminal_gone(fd))
return closed and (terminal is None or _terminal_gone(terminal))
def _terminal_gone(fd: int) -> bool:
"""Whether the terminal at fd hung up: a closed window answers EIO to
tcgetattr, a live one its settings."""
def _terminal_gone(terminal: _TuiTerminal) -> bool:
"""Whether the terminal hung up: a closed window answers EIO to
tcgetattr and to a write, a live one its settings."""
import termios # noqa: PLC0415
try:
termios.tcgetattr(fd)
except (termios.error, OSError):
termios.tcgetattr(terminal.fd)
terminal.stdout.write("")
terminal.stdout.flush()
except (termios.error, OSError, ValueError):
return True
return False
+9 -4
View File
@@ -2576,16 +2576,21 @@ class AReadErrorIsNotAClosedWindow(unittest.TestCase):
import pty
master, slave = pty.openpty()
screen = open(os.dup(slave), "w", encoding="utf-8") # noqa: SIM115
terminal = mock.Mock(fd=slave, stdout=screen)
try:
card = OSError(errno.EIO, "Input/output error", "/media/card/bios/x.bin")
self.assertFalse(install._closed_terminal(card, slave))
self.assertFalse(install._closed_terminal(card, terminal))
self.assertFalse(install._closed_terminal(
OSError(errno.ENOENT, "No such file"), slave))
OSError(errno.ENOENT, "No such file"), terminal))
os.close(master)
master = -1
self.assertTrue(install._closed_terminal(card, slave))
self.assertTrue(install._closed_terminal(termios.error(errno.EIO, "gone"), slave))
self.assertTrue(install._closed_terminal(card, terminal))
self.assertTrue(install._closed_terminal(termios.error(errno.EIO, "gone"),
terminal))
finally:
with contextlib.suppress(OSError):
screen.close()
os.close(slave)
if master >= 0:
os.close(master)