Artifact 845b21969a64c78596ac8d73a015e07eab11e47e4ceb63634f822eca6d832bf8:
- File
internal/xfer/message_oversize_test.go
— part of check-in
[2b9694f525]
at
2026-07-22 23:32:59
on branch trunk
— fix: xfer parse guards — degrade unparseable clone tokens (#95) and reject oversized §4.1 length before zlib (#110) (#124)
* fix: degrade unparseable clone VERSION/SEQNO instead of erroring (#95)
parseClone routed both tokens through strconv.Atoi before the §8.1 digit-only rule ever got a chance to apply, so a token Atoi could not parse at all (not just one that fails digit-only) hard-errored the whole message instead of degrading gracefully. Carry the raw tokens forward as an UnknownCard on parse failure, matching how UnknownCard already handles a non-decimal clone_seqno, so a later card in the same message still decodes.
* fix: reject an oversized §4.1 declared length before trying raw zlib (#110)
xfer.Decode tried the unprefixed-zlib framing before ever reading the §4.1 container's declared length prefix. A peer declaring a length at or above 0x08000000 produces a prefix whose first two bytes present as a structurally valid zlib CMF/FLG header (RFC 1950), so a large declared length aliases as the start of a raw zlib stream and gets first refusal on a legitimate compressed container. Read the declared length whenever the body opens with a valid zlib header and reject it against MaxDecompressedBytes before attempting format 1 at all. (user: [email protected], size: 7266) [annotate] [blame] [check-ins using]
package xfer import ( "bytes" "compress/zlib" "encoding/binary" "strings" "testing" ) // compressedContainer builds the §4.1 compressed container -- 4-byte // big-endian pre-compression length followed by a zlib stream -- around n // bytes of card text, without ever holding n bytes in memory. func compressedContainer(t *testing.T, n int) []byte { t.Helper() if n <= 0 { panic("compressedContainer: n must be positive") } var buf bytes.Buffer var prefix [4]byte binary.BigEndian.PutUint32(prefix[:], uint32(n)) buf.Write(prefix[:]) zw := zlib.NewWriter(&buf) // "igot <hash>\n" repeated: real cards, so a decoder that reaches them // fails for a reason other than the payload being unparseable. card := []byte("igot " + strings.Repeat("a", 40) + "\n") written := 0 for written < n { chunk := card if remaining := n - written; remaining < len(chunk) { chunk = chunk[:remaining] } if _, err := zw.Write(chunk); err != nil { t.Fatalf("zlib write: %v", err) } written += len(chunk) } if err := zw.Close(); err != nil { t.Fatalf("zlib close: %v", err) } if buf.Len() >= n { t.Fatalf("container did not compress: %d bytes for %d input", buf.Len(), n) } return buf.Bytes() } // TestDecodeOversizeCompressedMessageIsReported checks that a well-formed // compressed body larger than MaxDecompressedBytes is reported as an oversize // message. Method: build a valid container whose decompressed length exceeds // the bound and decode it. The regression this pins (issue #104) is not that // the decode fails -- it is *how* it failed: the previous fallback chain // treated the size rejection as "not this framing" and handed the still // compressed bytes to the card parser, which read compressed noise as cards // until one of them split into no fields, and reported a card-syntax error // roughly sixteen thousand cards into a body that held none. func TestDecodeOversizeCompressedMessageIsReported(t *testing.T) { if testing.Short() { t.Skip("inflates past MaxDecompressedBytes") } data := compressedContainer(t, MaxDecompressedBytes+1024) _, err := Decode(data) if err == nil { t.Fatal("Decode accepted a message over MaxDecompressedBytes") } if !strings.Contains(err.Error(), "exceeds") { t.Errorf("error does not name the size bound: %v", err) } if strings.Contains(err.Error(), "empty line after split") { t.Errorf("compressed bytes were parsed as cards: %v", err) } if strings.Contains(err.Error(), "decode card") { t.Errorf("transport failure reported as a card fault: %v", err) } } // TestDecodeCorruptZlibIsNotParsedAsCards checks that a body with a valid zlib // header and a corrupt body is reported as a decompression failure. Method: // truncate a valid container mid-stream, which leaves the header intact. func TestDecodeCorruptZlibIsNotParsedAsCards(t *testing.T) { full := compressedContainer(t, 1<<20) truncated := full[:len(full)/2] _, err := Decode(truncated) if err == nil { t.Fatal("Decode accepted a truncated zlib stream") } if strings.Contains(err.Error(), "decode card") { t.Errorf("truncated stream reported as a card fault: %v", err) } } // TestDecodeRoundTripsLargeMessageUnderBound checks that a message just under // the bound still decodes, so the guard cannot be satisfied by rejecting // everything large. Method: encode enough igot cards to pass 32 MiB and // decode the result. func TestDecodeRoundTripsLargeMessageUnderBound(t *testing.T) { if testing.Short() { t.Skip("encodes a 32 MiB message") } const target = 32 << 20 msg := &Message{} for size := 0; size < target; size += 46 { msg.Cards = append(msg.Cards, &IGotCard{UUID: strings.Repeat("b", 40)}) } wire, err := msg.Encode() if err != nil { t.Fatalf("Encode: %v", err) } got, err := Decode(wire) if err != nil { t.Fatalf("Decode: %v", err) } if len(got.Cards) != len(msg.Cards) { t.Errorf("decoded %d cards, want %d", len(got.Cards), len(msg.Cards)) } } // TestDecodeUncompressedBodyStillWorks checks that plain card text is still // accepted, since the fix narrows when that fallback is reached. Method: // decode an uncompressed body of the kind an x-fossil-uncompressed clone // reply carries. func TestDecodeUncompressedBodyStillWorks(t *testing.T) { body := []byte("igot " + strings.Repeat("c", 40) + "\nclone_seqno 7\n") msg, err := Decode(body) if err != nil { t.Fatalf("Decode: %v", err) } if len(msg.Cards) != 2 { t.Fatalf("decoded %d cards, want 2", len(msg.Cards)) } if _, ok := msg.Cards[1].(*CloneSeqNoCard); !ok { t.Errorf("second card is %T, want *CloneSeqNoCard", msg.Cards[1]) } } // TestMaxDecompressedBytesIsReachable guards the bound against being set below // what a zlib stream can actually deliver here. Method: decompress a container // sized exactly at the bound. func TestMaxDecompressedBytesIsReachable(t *testing.T) { if testing.Short() { t.Skip("inflates MaxDecompressedBytes") } data := compressedContainer(t, MaxDecompressedBytes) raw, err := decompressContainer(data) if err != nil { t.Fatalf("decompressContainer at the bound: %v", err) } if len(raw) != MaxDecompressedBytes { t.Errorf("decompressed %d bytes, want %d", len(raw), MaxDecompressedBytes) } } // TestDecodeRejectsAliasingDeclaredLength pins issue #110: a §4.1 container // whose declared length's top byte presents as a valid zlib CMF byte (i.e. it // would alias as a format-1 header) must be rejected on the declared length // alone, before format 1 ever gets a chance to (mis)parse it. Method: build a // genuine container -- 4-byte declared length plus a real zlib payload -- but // give it a declared length at 0x081D0000 (>= zlibCMFAliasBytes, and known // from the issue's own probe to present a valid header at offset 0) and // assert Decode rejects it rather than returning whatever format 1 produces. func TestDecodeRejectsAliasingDeclaredLength(t *testing.T) { real := compressedContainer(t, 64) // small, genuine §4.1 container // Overwrite the genuine declared-length prefix with an aliasing one; the // zlib payload after it is untouched and still real. aliasing := append([]byte{0x08, 0x1D, 0x00, 0x00}, real[4:]...) _, err := decompressContainer(aliasing) if err == nil { t.Fatal("decompressContainer accepted a container with an aliasing declared length") } if !strings.Contains(err.Error(), "declared") { t.Errorf("error does not name the declared-length rejection: %v", err) } if _, err := Decode(aliasing); err == nil { t.Fatal("Decode accepted a container with an aliasing declared length") } } // TestDecodeUncompressedBodyWithLargeLeadingBytesStillWorks guards the fix for // #110 against over-rejecting: plain uncompressed card text whose first four // bytes happen to read as a large integer (ordinary ASCII often does) must // still fall through to format 3, since it never presented a zlib header at // all and so was never actually ambiguous with the §4.1 container. func TestDecodeUncompressedBodyWithLargeLeadingBytesStillWorks(t *testing.T) { body := []byte("igot " + strings.Repeat("c", 40) + "\nclone_seqno 7\n") msg, err := Decode(body) if err != nil { t.Fatalf("Decode: %v", err) } if len(msg.Cards) != 2 { t.Fatalf("decoded %d cards, want 2", len(msg.Cards)) } }