From a47a12e628830e9cce81c8933f7196735cec10a1 Mon Sep 17 00:00:00 2001 From: Yura Lazarev Date: Tue, 24 Mar 2026 14:28:59 +0100 Subject: [PATCH] fix: handle multi-byte advance in consClose decoder consClose only ever advanced currPtr by 1 byte. When the constructor tag bits plus the already-consumed bits in the current byte totalled 16 or more (e.g. a 9-bit tag after 7 bits already used), usedBits overflowed past 7 and the decoder state was silently corrupted. The immediate symptom: dBool computes (128 >> usedBits) as 0 when usedBits >= 8, so every bit reads as False. The Filler decoder then loops forever building FillerBit constructors until memory runs out. Replace the hand-rolled if/else with dropBits, which already uses divMod to handle any number of bytes correctly. Add three regression tests: - control: 7 Bool fields + 8-bit tag (15 total bits, no overflow) - bug trigger: 7 Bool fields + 9-bit tag (16 bits, infinite loop without fix, guarded by a 5 s timeout) - bounds check: 9-bit tag in a 1-byte buffer must fail with NotEnoughSpace, not TooMuchSpace --- src/Flat/Decoder/Prim.hs | 13 +++---------- test/Spec.hs | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Flat/Decoder/Prim.hs b/src/Flat/Decoder/Prim.hs index 0fde591..3fc28ba 100644 --- a/src/Flat/Decoder/Prim.hs +++ b/src/Flat/Decoder/Prim.hs @@ -104,17 +104,10 @@ consOpen = Get $ \endPtr s -> do else notEnoughSpace endPtr s return $ GetResult s (ConsState w 0) --- | Switch back to normal decoding --- {-# NOINLINE consClose #-} +-- | Switch back to normal decoding by advancing the main stream +-- past the constructor tag bits that were decoded via 'ConsState'. consClose :: Int -> Get () -consClose n = Get $ \endPtr s -> do - let u' = n + usedBits s - if u' < 8 - then return $ GetResult (s {usedBits = u'}) () - else - if currPtr s >= endPtr - then notEnoughSpace endPtr s - else return $ GetResult (s {currPtr = currPtr s `plusPtr` 1, usedBits = u' - 8}) () +consClose = dropBits {- ensureBits endPtr s n = when ((endPtr `minusPtr` currPtr s) * 8 - usedBits s < n) $ notEnoughSpace endPtr s dropBits8 s n = diff --git a/test/Spec.hs b/test/Spec.hs index d62547d..60bb051 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -348,6 +348,34 @@ testLargeEnum = testGroup "test enum with more than 256 constructors" -- , encRaw (E258_256,E258_257,E258_258) [0b11111110,0b11111111,0b01111111,0b11000000] , map trip [E258_1, E258_256, E258_257, E258_258] , map trip [E256_1, E256_134, E256_256] + -- Issue #7542: consClose only advances currPtr by 1 byte, so when + -- (constructor_bits + usedBits >= 16) the decoder state is corrupted + -- (usedBits overflows to 8+). This makes the Filler decoder loop + -- forever, building infinite FillerBit chains and consuming all memory. + -- + -- The bug requires: (a) a constructor needing 9 bits (depth-9 in the + -- Generic tree), AND (b) 7 prior consumed bits so usedBits=7 before + -- consOpen. We use 7 nested Bool fields to set up condition (b). + -- + -- Control: E258_256 needs only 8 bits, so 8+7=15 < 16 - no overflow. + , [trip (False, (False, (False, (False, (False, (False, (False, E258_256)))))))] + -- Bug: E258_258 needs 9 bits, so 9+7=16 - consClose overflows usedBits. + -- Without fix: unflat hangs forever (Filler decoder infinite loop). + , [localOption (mkTimeout 5000000) $ + trip (False, (False, (False, (False, (False, (False, (False, E258_258)))))))] + -- consClose must reject when constructor bits exceed the remaining buffer, + -- not silently leave the decoder in an invalid state for strictDecoder to + -- catch later. E258_258 needs 9 bits but a 1-byte buffer only has 8. + -- With correct bounds checking (ensureBits), consClose throws NotEnoughSpace. + -- Without it, consClose "succeeds" and strictDecoder catches TooMuchSpace. + , [testCase "consClose rejects 9-bit tag in 1-byte buffer" $ + case unflatRaw (B.pack [0xFF]) :: Decoded E258 of + Left (NotEnoughSpace _) -> return () + Left (TooMuchSpace _) -> assertFailure + "consClose let overrun through (caught by strictDecoder as TooMuchSpace)" + Left other -> assertFailure $ "Unexpected error: " ++ show other + Right _ -> assertFailure "Should not decode: only 8 bits for a 9-bit tag" + ] #endif ]