Skip to content

Fix #1070: bytes_to_read buffer size for Python 3.14 - #1074

Open
RachelXiaolan wants to merge 2 commits into
pyinvoke:mainfrom
RachelXiaolan:ai-2138-round-5-bytes-to-read
Open

Fix #1070: bytes_to_read buffer size for Python 3.14#1074
RachelXiaolan wants to merge 2 commits into
pyinvoke:mainfrom
RachelXiaolan:ai-2138-round-5-bytes-to-read

Conversation

@RachelXiaolan

Copy link
Copy Markdown

Fixes #1070.

Bug

On Python 3.14, fcntl.ioctl rejects undersized mutable buffers (cpython#144206). bytes_to_read() was passing a 2-byte buffer to FIONREAD, which writes a C int (typically 4 bytes). Same root cause as the bug fixed in #1038 for _pty_size, but bytes_to_read was missed.

Worked by accident on <=3.13 thanks to CPython's 1024-byte static copy buffer. On 3.14, Connection.run() from a TTY-attached process crashes with SystemError: buffer overflow in handle_stdin.

Fix

Pass a 4-byte buffer and unpack as a signed int:

fionread = fcntl.ioctl(input_, termios.FIONREAD, b"\x00\x00\x00\x00")
return int(struct.unpack("i", fionread)[0])

Tests

tests/terminals/test_bytes_to_read.py — 3 cases:

  • buffer passed to ioctl is 4 bytes
  • result is the int parsed from the 4-byte C int
  • FIONREAD returning 0 is correctly handled
  • non-TTY fallback still returns 1

Refs: AI-2138

Fixes pyinvoke#1070.

On Python 3.14, fcntl.ioctl rejects undersized mutable buffers
(cpython#144206). `bytes_to_read()` was passing a 2-byte buffer to
FIONREAD, which writes a C int (typically 4 bytes). The same bug was
fixed for `_pty_size` in pyinvoke#1038, but `bytes_to_read` was missed.

This worked by accident on <=3.13 thanks to CPython's 1024-byte static
copy buffer that masked the size mismatch. CPython 3.14 tightened the
check, so `Connection.run()` from a TTY-attached process on 3.14 now
crashes with SystemError: buffer overflow in `handle_stdin`.

Fix: pass a 4-byte buffer (`b"\x00\x00\x00\x00"`) and unpack as a
signed int (`struct.unpack('i', ...)`) instead of signed short
(`'h'`).

Tests: tests/terminals/test_bytes_to_read.py covers:
- buffer passed to ioctl is 4 bytes
- result is the int parsed from the 4-byte C int
- FIONREAD returning 0 is correctly handled
- non-TTY fallback still returns 1

Test runs:
- New tests: 3/3 pass
- Full non-pty, non-runner suite: 774 passed (no regressions)
- Pre-existing pty/runner failures in this sandbox are env-specific
  (no real TTY, no /dev/tty) and unrelated to this change

Fixes pyinvoke#1070
Refs: AI-2138
@hefee

hefee commented Aug 12, 2026

Copy link
Copy Markdown

The test file look very complicated and not useful.

Replace the standalone test file with additions to the existing
tests/terminals.py, matching the project's class-based test convention.
Remove the over-engineered test_bytes_to_read.py.

Fixes feedback from @hefee about test complexity.
@RachelXiaolan

Copy link
Copy Markdown
Author

Thanks for the feedback @hefee! You're right — I overcomplicated it.

I've removed the standalone test file and added the test cases directly to the existing tests/terminals.py, matching the project's existing style (class-based, @patch decorators). The new tests are:

  • uses_4_byte_buffer_for_FIONREAD — verifies ioctl gets a 4-byte buffer and the result is correct
  • returns_0_when_FIONREAD_is_zero — edge case for zero bytes available

Both follow the same pattern as the existing pty_size and bytes_to_read_ tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bytes_to_read() raises SystemError: buffer overflow on Python 3.14 (FIONREAD undersized buffer)

3 participants