@@ -413,24 +413,24 @@ def test_rejects_oversized_message(self) -> None:
413413 with pytest .raises (DecodeError , match = "exceeds maximum" ):
414414 buf .read_message ()
415415
416- def test_skip_message_recovers_from_oversized (self ) -> None :
417- """After skipping an oversized message, stream should not be corrupted.
416+ def test_skip_message_poisons_after_capped_oversized (self ) -> None :
417+ """After a capped oversized skip, stream is desynchronized and
418+ the buffer must be poisoned (issue 121).
418419
419420 skip_message caps _skip_remaining to max_message_size to prevent
420- amplification attacks. The test feeds at most max_message_size bytes
421- of oversized body, then verifies the next message decodes correctly.
421+ amplification attacks. Since the peer sent more bytes than the cap,
422+ the stream is permanently desynchronized. The buffer is poisoned
423+ once the capped skip completes.
422424 """
423425 import struct
424426
425427 import pytest
426428
427- from dqlitewire .exceptions import DecodeError
429+ from dqlitewire .exceptions import DecodeError , ProtocolError
428430
429431 buf = ReadBuffer (max_message_size = 1024 )
430432 # Header claiming a huge body: size_words=1000 (8000 bytes > 1024 limit)
431433 oversized_header = struct .pack ("<IBBH" , 1000 , 0 , 0 , 0 )
432- valid_msg = LeaderResponse (node_id = 1 , address = "node1:9001" )
433- valid_encoded = valid_msg .encode ()
434434
435435 # Feed header + first chunk
436436 buf .feed (oversized_header + b"\xab " * 500 )
@@ -443,25 +443,60 @@ def test_skip_message_recovers_from_oversized(self) -> None:
443443 # skip_message should return False — partial skip
444444 assert buf .skip_message () is False
445445
446- # Feed enough bytes to complete the capped skip + valid message.
447- # _skip_remaining is capped to max_message_size (1024), and skip_message
448- # already consumed the 508 bytes in the buffer (8 header + 500 body),
449- # so _skip_remaining = 1024 - 508 = 516 bytes.
450- buf .feed (b"\xab " * 516 + valid_encoded )
446+ # Feed enough bytes to complete the capped skip.
447+ remaining = buf ._skip_remaining
448+ buf .feed (b"\xab " * remaining )
451449
452- # The capped oversized bytes should be discarded; valid message readable
453- assert buf .has_message ()
454- data = buf .read_message ()
455- assert data is not None
456- assert data == valid_encoded
450+ # Skip is complete but buffer is poisoned — stream is desynchronized
451+ assert not buf .is_skipping
452+ assert buf .is_poisoned
453+
454+ with pytest .raises (ProtocolError , match = "poisoned" ):
455+ buf .read_message ()
457456
458- def test_single_feed_completes_skip_and_appends_message (self ) -> None :
459- """118: single feed() that completes skip AND contains next valid message."""
457+ def test_capped_oversized_skip_poisons_buffer (self ) -> None :
458+ """121: capped skip of oversized message must poison the buffer.
459+
460+ When the header claims a body larger than max_message_size, the skip
461+ is capped to max_message_size bytes. Since the peer sent more bytes
462+ than were discarded, the stream is permanently desynchronized. The
463+ buffer must be poisoned to prevent silent garbage reads.
464+ """
460465 import struct
461466
462467 import pytest
463468
464- from dqlitewire .exceptions import DecodeError
469+ from dqlitewire .exceptions import DecodeError , ProtocolError
470+
471+ buf = ReadBuffer (max_message_size = 64 )
472+ # Header claiming 200 words = 1600 bytes body (>> 64 byte limit)
473+ oversized_header = struct .pack ("<IBBH" , 200 , 0 , 0 , 0 )
474+ buf .feed (oversized_header )
475+
476+ assert buf .has_message () is True
477+ with pytest .raises (DecodeError , match = "exceeds maximum" ):
478+ buf .read_message ()
479+
480+ # skip_message triggers capped skip
481+ buf .skip_message ()
482+
483+ # After the capped skip completes, buffer must be poisoned
484+ remaining = buf ._skip_remaining
485+ buf .feed (b"\x00 " * remaining )
486+ assert not buf .is_skipping
487+ assert buf .is_poisoned
488+
489+ # Further operations should raise ProtocolError
490+ with pytest .raises (ProtocolError , match = "poisoned" ):
491+ buf .read_message ()
492+
493+ def test_single_feed_completes_capped_skip_and_poisons (self ) -> None :
494+ """118/121: single feed() that completes capped skip poisons buffer."""
495+ import struct
496+
497+ import pytest
498+
499+ from dqlitewire .exceptions import DecodeError , ProtocolError
465500
466501 buf = ReadBuffer (max_message_size = 64 )
467502 # 200 words = 1600 bytes body, well over 64-byte limit
@@ -474,37 +509,34 @@ def test_single_feed_completes_skip_and_appends_message(self) -> None:
474509 assert buf .skip_message () is False
475510 assert buf .is_skipping
476511
477- # Build a single payload: remaining skip bytes + valid message
512+ # Build a single payload: remaining skip bytes + extra data
478513 remaining = buf ._skip_remaining
479- valid_msg = LeaderRequest ()
480- valid_encoded = valid_msg .encode ()
481- combined = b"\xcc " * remaining + valid_encoded
514+ combined = b"\xcc " * remaining + b"\x00 " * 16
482515
483516 buf .feed (combined )
484517
485518 assert not buf .is_skipping
486- assert buf .has_message ()
487- data = buf . read_message ()
488- assert data is not None
489- assert data == valid_encoded
519+ assert buf .is_poisoned
520+
521+ with pytest . raises ( ProtocolError , match = "poisoned" ):
522+ buf . read_message ()
490523
491- def test_skip_oversized_across_multiple_feeds (self ) -> None :
492- """Oversized message bytes should be discarded across multiple feed() calls .
524+ def test_skip_oversized_across_multiple_feeds_poisons (self ) -> None :
525+ """Oversized skip across multiple feeds poisons when complete (issue 121) .
493526
494527 skip_message caps _skip_remaining to max_message_size, so only that
495- many bytes are discarded (not the full claimed body size).
528+ many bytes are discarded. Once the capped skip completes, the buffer
529+ is poisoned because the stream is desynchronized.
496530 """
497531 import struct
498532
499533 import pytest
500534
501- from dqlitewire .exceptions import DecodeError
535+ from dqlitewire .exceptions import DecodeError , ProtocolError
502536
503537 buf = ReadBuffer (max_message_size = 64 )
504538 # 200 words = 1600 bytes body, well over 64-byte limit
505539 oversized_header = struct .pack ("<IBBH" , 200 , 0 , 0 , 0 )
506- valid_msg = LeaderRequest ()
507- valid_encoded = valid_msg .encode ()
508540
509541 # Feed just the header
510542 buf .feed (oversized_header )
@@ -513,20 +545,20 @@ def test_skip_oversized_across_multiple_feeds(self) -> None:
513545 buf .read_message ()
514546 assert buf .skip_message () is False
515547
516- # Feed capped body in chunks (max_message_size=64, header was 8 bytes,
517- # so _skip_remaining = 64 - 8 = 56 bytes)
548+ # Feed capped body in chunks
518549 remaining = buf ._skip_remaining
519550 body = b"\xcc " * remaining
520551 while body :
521552 chunk = body [:20 ]
522553 body = body [20 :]
523554 buf .feed (chunk )
524555
525- # Now feed the valid message
526- buf .feed (valid_encoded )
527- assert buf .has_message ()
528- data = buf .read_message ()
529- assert data == valid_encoded
556+ # Capped skip is complete — buffer should be poisoned
557+ assert not buf .is_skipping
558+ assert buf .is_poisoned
559+
560+ with pytest .raises (ProtocolError , match = "poisoned" ):
561+ buf .feed (b"\x00 " * 16 )
530562
531563 def test_is_skipping_property (self ) -> None :
532564 """is_skipping reflects whether an oversized skip is in progress."""
0 commit comments