Skip to content

Commit 5647b89

Browse files
Enforce the parameter-count cap on encode_params_tuple
decode_params_tuple caps the decoded parameter count at _MAX_PARAM_COUNT (100_000) as defense in depth; the encoder did not mirror the same check for schema 1 (uint32 count). V0 has a 255 cap from the 1-byte count field, but V1 is selected automatically whenever the caller passes more than 255 parameters, so an accidental giant params sequence burned an unbounded amount of allocation before the 64 MiB frame cap eventually fired with an opaque "buffer size exceeds maximum" error. Raise EncodeError with the same "exceeds maximum" shape the decoder uses, placed after schema validation so the check fires uniformly regardless of the short-circuit paths for empty params sequences. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0f2e583 commit 5647b89

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

src/dqlitewire/tuples.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ def encode_params_tuple(params: Sequence[Any], schema: int = 0, buffer_offset: i
5252
if schema not in (0, 1):
5353
raise EncodeError(f"Unsupported params tuple schema version: {schema} (expected 0 or 1)")
5454

55+
# Mirror the decoder's defense-in-depth cap so that a caller who
56+
# accidentally passes an enormous params sequence (e.g. from a bug
57+
# that builds WHERE IN (?,...) with millions of placeholders) fails
58+
# fast with a clear message instead of burning allocations until the
59+
# 64 MiB frame cap fires with an opaque "buffer size exceeds maximum"
60+
# error.
61+
if len(params) > _MAX_PARAM_COUNT:
62+
raise EncodeError(f"Parameter count {len(params)} exceeds maximum ({_MAX_PARAM_COUNT})")
63+
5564
if not params:
5665
# Go writes nothing for empty params
5766
return b""

tests/test_tuples.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,3 +614,22 @@ def test_v0_count_within_cap_succeeds(self) -> None:
614614
data = b"\x00" + b"\x00" * 7 # count=0, padded to 8 bytes
615615
result, _ = decode_params_tuple(data, schema=0)
616616
assert result == []
617+
618+
619+
class TestParamsTupleEncoderMaxCount:
620+
"""encode_params_tuple should mirror decode_params_tuple's cap."""
621+
622+
def test_encoder_rejects_excess_count(self) -> None:
623+
from dqlitewire.tuples import _MAX_PARAM_COUNT
624+
625+
params = [0] * (_MAX_PARAM_COUNT + 1)
626+
with pytest.raises(EncodeError, match="exceeds maximum"):
627+
encode_params_tuple(params, schema=1)
628+
629+
def test_exec_request_rejects_excess_params(self) -> None:
630+
from dqlitewire.messages.requests import ExecRequest
631+
from dqlitewire.tuples import _MAX_PARAM_COUNT
632+
633+
req = ExecRequest(db_id=1, stmt_id=1, params=[0] * (_MAX_PARAM_COUNT + 1))
634+
with pytest.raises(EncodeError, match="exceeds maximum"):
635+
req.encode()

0 commit comments

Comments
 (0)