Skip to content

Commit 64247d0

Browse files
fix: StmtResponse rejects short body when schema=1 claimed
Decode previously accepted schema=1 bodies as short as 16 bytes, silently producing tail_offset=None — indistinguishable from a correct V0 body. A peer sending schema=1 but omitting tail_offset would look identical to one sending schema=0. Require 24 bytes when schema>=1 and 16 bytes when schema=0; raise DecodeError otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c94bcd2 commit 64247d0

2 files changed

Lines changed: 24 additions & 4 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,15 @@ def encode_body(self) -> bytes:
179179

180180
@classmethod
181181
def decode_body(cls, data: bytes, schema: int = 0) -> "StmtResponse":
182+
expected = 24 if schema >= 1 else 16
183+
if len(data) < expected:
184+
raise DecodeError(
185+
f"StmtResponse schema={schema} requires at least {expected} bytes, got {len(data)}"
186+
)
182187
db_id = decode_uint32(data)
183188
stmt_id = decode_uint32(data[4:])
184189
num_params = decode_uint64(data[8:])
185-
tail_offset: int | None = None
186-
if schema >= 1 and len(data) >= 24:
187-
tail_offset = decode_uint64(data[16:])
190+
tail_offset = decode_uint64(data[16:]) if schema >= 1 else None
188191
return cls(db_id, stmt_id, num_params, tail_offset)
189192

190193

tests/test_messages_responses.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from dqlitewire.constants import HEADER_SIZE, ResponseType, ValueType
6-
from dqlitewire.exceptions import EncodeError
6+
from dqlitewire.exceptions import DecodeError, EncodeError
77
from dqlitewire.messages.base import Header
88
from dqlitewire.messages.responses import (
99
DbResponse,
@@ -134,6 +134,23 @@ def test_v0_has_no_tail_offset(self) -> None:
134134
decoded = StmtResponse.decode_body(encoded[HEADER_SIZE:])
135135
assert decoded.tail_offset is None
136136

137+
def test_schema_0_rejects_short_body(self) -> None:
138+
"""235: Schema=0 body must be at least 16 bytes."""
139+
with pytest.raises(DecodeError, match="requires at least 16 bytes"):
140+
StmtResponse.decode_body(b"\x00" * 8, schema=0)
141+
142+
def test_schema_1_rejects_v0_length_body(self) -> None:
143+
"""235: Schema=1 demands 24 bytes.
144+
145+
A 16-byte body under schema=1 was previously accepted silently and
146+
returned ``tail_offset=None``, indistinguishable from a real V0
147+
body. That collapsed two different protocol states into one.
148+
"""
149+
v0_body = b"\x01\x00\x00\x00" + b"\x02\x00\x00\x00" + b"\x03" + b"\x00" * 7
150+
assert len(v0_body) == 16
151+
with pytest.raises(DecodeError, match="requires at least 24 bytes"):
152+
StmtResponse.decode_body(v0_body, schema=1)
153+
137154

138155
class TestResultResponse:
139156
def test_roundtrip(self) -> None:

0 commit comments

Comments
 (0)