diff --git a/plutus-core/changelog.d/20260320_154057_yuriy.lazaryev_issue_7542_flat_enum_bug.md b/plutus-core/changelog.d/20260320_154057_yuriy.lazaryev_issue_7542_flat_enum_bug.md new file mode 100644 index 00000000000..ba3cd485c75 --- /dev/null +++ b/plutus-core/changelog.d/20260320_154057_yuriy.lazaryev_issue_7542_flat_enum_bug.md @@ -0,0 +1,3 @@ +### Fixed + +- Fixed `consClose` in the Flat decoder failing to advance the stream pointer past 1 byte, which corrupted decoder state for Generic-derived Flat instances of large enums (>256 constructors) and caused infinite memory consumption during deserialization. diff --git a/plutus-core/flat/src/PlutusCore/Flat/Decoder/Prim.hs b/plutus-core/flat/src/PlutusCore/Flat/Decoder/Prim.hs index a3e37db50fc..319ed7040a3 100644 --- a/plutus-core/flat/src/PlutusCore/Flat/Decoder/Prim.hs +++ b/plutus-core/flat/src/PlutusCore/Flat/Decoder/Prim.hs @@ -103,17 +103,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/plutus-core/flat/test/Spec.hs b/plutus-core/flat/test/Spec.hs index 522e2e5a067..5f66fcab7fe 100644 --- a/plutus-core/flat/test/Spec.hs +++ b/plutus-core/flat/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 ]