RustPython/RustPython · error · ValueError

readexactly size can not be less than zero

Error message

readexactly size can not be less than zero

What it means

StreamReader.readexactly(n) requires a non-negative byte count; n < 0 raises ValueError('readexactly size can not be less than zero') before any I/O happens. n == 0 is legal and immediately returns b''. The guard protects the buffer arithmetic (which slices and compares against n) from nonsensical sizes.

Source

Thrown at Lib/asyncio/streams.py:755

        return data

    async def readexactly(self, n):
        """Read exactly `n` bytes.

        Raise an IncompleteReadError if EOF is reached before `n` bytes can be
        read. The IncompleteReadError.partial attribute of the exception will
        contain the partial read bytes.

        if n is zero, return empty bytes object.

        Returned value is not limited with limit, configured at stream
        creation.

        If stream was paused, this function will automatically resume it if
        needed.
        """
        if n < 0:
            raise ValueError('readexactly size can not be less than zero')

        if self._exception is not None:
            raise self._exception

        if n == 0:
            return b''

        while len(self._buffer) < n:
            if self._eof:
                incomplete = bytes(self._buffer)
                self._buffer.clear()
                raise exceptions.IncompleteReadError(incomplete, n)

            await self._wait_for_data('readexactly')

        if len(self._buffer) == n:
            data = bytes(self._buffer)
            self._buffer.clear()

View on GitHub (pinned to aaeab4f754)

Solutions

  1. Validate the parsed length immediately: if n < 0 raise a protocol error naming the field and the raw bytes decoded.
  2. Use unsigned struct formats ('<I', '<Q') for on-wire lengths so negative values cannot be decoded.
  3. Add an upper bound too: reject n above a max message size before waiting, which also caps memory.
  4. If 'read the rest' semantics are wanted, use reader.read(-1), not readexactly(-1).

Example fix

# before
n = struct.unpack('<i', await reader.readexactly(4))[0]  # signed -> -1 possible
data = await reader.readexactly(n)                      # ValueError

# after
n = struct.unpack('<I', await reader.readexactly(4))[0]  # unsigned
if n > MAX_MSG:
    raise ProtocolError(f'frame too large: {n}')
data = await reader.readexactly(n)
Defensive patterns

Strategy: validation

Validate before calling

def parse_frame_length(raw: bytes) -> int:
    n = struct.unpack('<I', raw)[0]  # unsigned
    if n > MAX_MSG:
        raise ProtocolError(f'frame too large: {n}')
    return n

n = parse_frame_length(await reader.readexactly(4))
assert n >= 0  # guaranteed by '<I'
data = await reader.readexactly(n)

Try / catch

try:
    data = await reader.readexactly(n)
except ValueError as e:
    if 'less than zero' in str(e):
        raise ProtocolError(f'peer sent negative frame length {n}') from e
    raise

Prevention

When it happens

Trigger: await reader.readexactly(-1); length prefixes decoded from a wire protocol into a negative number (signed vs unsigned struct mismatch like '<i' vs '<I'); computed sizes such as remaining = total - already_read going negative due to bad accounting; sentinel -1 passed to mean 'rest of stream'.

Common situations: Binary protocol clients misparsing headers (sign bit, endianness); fuzzed or malicious input crafting negative lengths; peers lying about frame size; unit tests using -1 sentinels; refactors where read() semantics (-1 allowed) were copied onto readexactly.

Related errors


AI-assisted analysis of RustPython/RustPython@aaeab4f754 (2026-08-17). Data as JSON: /api/errors/be86c70980d936f6. Report an issue: GitHub.