diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,11 @@
 # Changelog
 
-- 0.3.1 (2026-02-01)
+- 0.3.2 (2026-11-14)
+  * 'new' and 'reseed' combine their seed material in a pinned buffer
+    that is zeroed after use, rather than concatenating it into
+    ordinary heap bytestrings that could outlive 'wipe'.
+
+- 0.3.1 (2026-02-04)
   * Adds SPECIALIZE pragmas for IO and ST to improve the generated Core.
 
 - 0.3.0 (2026-02-01)
diff --git a/lib/Crypto/DRBG/HMAC/Internal.hs b/lib/Crypto/DRBG/HMAC/Internal.hs
--- a/lib/Crypto/DRBG/HMAC/Internal.hs
+++ b/lib/Crypto/DRBG/HMAC/Internal.hs
@@ -1,4 +1,5 @@
 {-# OPTIONS_HADDOCK hide #-}
+{-# LANGUAGE BangPatterns #-}
 
 -- |
 -- Module: Crypto.DRBG.HMAC.Internal
@@ -12,9 +13,18 @@
     Error(..)
   , _RESEED_COUNTER
   , _MAX_BYTES
+  , _with_concat
   ) where
 
-import Data.Word (Word64)
+import Control.Monad.Primitive (PrimMonad)
+import qualified Control.Monad.Primitive as Prim (unsafeIOToPrim)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Internal as BI
+import qualified Data.ByteString.Unsafe as BU
+import Data.Word (Word8, Word64)
+import Foreign.ForeignPtr (withForeignPtr)
+import Foreign.Marshal.Utils (copyBytes, fillBytes)
+import Foreign.Ptr (Ptr, castPtr, plusPtr)
 
 -- | A DRBG error.
 data Error =
@@ -31,3 +41,29 @@
 _MAX_BYTES :: Word64
 _MAX_BYTES = 0x10000
 {-# INLINE _MAX_BYTES #-}
+
+-- | Run an action on the concatenation of the given strings, held in a
+--   pinned buffer that is zeroed once the action completes, so that
+--   (secret) seed material leaves no copy of the library's making
+--   behind. The action must not retain its argument.
+_with_concat
+  :: PrimMonad m
+  => [BS.ByteString]
+  -> (BS.ByteString -> m a)
+  -> m a
+_with_concat bss act = do
+  let !n = sum (fmap BS.length bss)
+  fp <- Prim.unsafeIOToPrim $ do
+    fp <- BI.mallocByteString n
+    withForeignPtr fp $ \dst -> copy_all dst bss
+    pure fp
+  !r <- act (BI.BS fp n)
+  Prim.unsafeIOToPrim $ withForeignPtr fp $ \p -> fillBytes p 0 n
+  pure r
+  where
+    copy_all :: Ptr Word8 -> [BS.ByteString] -> IO ()
+    copy_all _ [] = pure ()
+    copy_all dst (b@(BI.PS _ _ l) : bs) = do
+      BU.unsafeUseAsCString b $ \src -> copyBytes dst (castPtr src) l
+      copy_all (dst `plusPtr` l) bs
+{-# INLINE _with_concat #-}
diff --git a/lib/Crypto/DRBG/HMAC/SHA256.hs b/lib/Crypto/DRBG/HMAC/SHA256.hs
--- a/lib/Crypto/DRBG/HMAC/SHA256.hs
+++ b/lib/Crypto/DRBG/HMAC/SHA256.hs
@@ -29,7 +29,8 @@
   , _read_k
   ) where
 
-import Crypto.DRBG.HMAC.Internal (Error(..), _RESEED_COUNTER, _MAX_BYTES)
+import Crypto.DRBG.HMAC.Internal
+  (Error(..), _RESEED_COUNTER, _MAX_BYTES, _with_concat)
 import qualified Crypto.Hash.SHA256 as SHA256
 import Crypto.Hash.SHA256.Internal (Registers(..))
 import qualified Crypto.Hash.SHA256.Internal as SHA256 (cat)
@@ -87,7 +88,7 @@
   PA.setPrimArray drbg 02 08 (0x00000000 :: Word32) -- init k
   PA.setPrimArray drbg 10 08 (0x01010101 :: Word32) -- init v
   PA.setPrimArray drbg 18 16 (0x00000000 :: Word32) -- scratch
-  update drbg (entropy <> nonce <> ps)
+  _with_concat [entropy, nonce, ps] (update drbg)
   pure $! DRBG drbg
 {-# INLINABLE new #-}
 {-# SPECIALIZE new
@@ -118,7 +119,7 @@
   -> BS.ByteString
   -> m ()
 reseed (DRBG drbg) entr addl = do
-  update drbg (entr <> addl)
+  _with_concat [entr, addl] (update drbg)
   init_counter drbg
 {-# INLINE reseed #-}
 
@@ -184,7 +185,9 @@
 -- | Wipe the state of a DRBG.
 --
 --   You should call this when you're finished with a DRBG to ensure that its
---   state is wiped from memory.
+--   state is wiped from memory. A wiped DRBG must not be used afterwards:
+--   its state is reset to the publicly known initial one, so it would
+--   produce predictable output.
 --
 --   >>> drbg <- new mempty mempty mempty
 --   >>> Right bytes <- gen drbg addl_bytes 16
diff --git a/lib/Crypto/DRBG/HMAC/SHA512.hs b/lib/Crypto/DRBG/HMAC/SHA512.hs
--- a/lib/Crypto/DRBG/HMAC/SHA512.hs
+++ b/lib/Crypto/DRBG/HMAC/SHA512.hs
@@ -29,7 +29,8 @@
   , _read_k
   ) where
 
-import Crypto.DRBG.HMAC.Internal (Error(..), _RESEED_COUNTER, _MAX_BYTES)
+import Crypto.DRBG.HMAC.Internal
+  (Error(..), _RESEED_COUNTER, _MAX_BYTES, _with_concat)
 import qualified Crypto.Hash.SHA512 as SHA512
 import Crypto.Hash.SHA512.Internal (Registers(..))
 import qualified Crypto.Hash.SHA512.Internal as SHA512 (cat)
@@ -87,7 +88,7 @@
   PA.setPrimArray drbg 01 08 (0x0000000000000000 :: Word64) -- init k
   PA.setPrimArray drbg 09 08 (0x0101010101010101 :: Word64) -- init v
   PA.setPrimArray drbg 17 16 (0x0000000000000000 :: Word64) -- scratch
-  update drbg (entropy <> nonce <> ps)
+  _with_concat [entropy, nonce, ps] (update drbg)
   pure $! DRBG drbg
 {-# INLINABLE new #-}
 {-# SPECIALIZE new
@@ -118,7 +119,7 @@
   -> BS.ByteString
   -> m ()
 reseed (DRBG drbg) entr addl = do
-  update drbg (entr <> addl)
+  _with_concat [entr, addl] (update drbg)
   init_counter drbg
 {-# INLINE reseed #-}
 
@@ -184,7 +185,9 @@
 -- | Wipe the state of a DRBG.
 --
 --   You should call this when you're finished with a DRBG to ensure that its
---   state is wiped from memory.
+--   state is wiped from memory. A wiped DRBG must not be used afterwards:
+--   its state is reset to the publicly known initial one, so it would
+--   produce predictable output.
 --
 --   >>> drbg <- new mempty mempty mempty
 --   >>> Right bytes <- gen drbg addl_bytes 16
diff --git a/ppad-hmac-drbg.cabal b/ppad-hmac-drbg.cabal
--- a/ppad-hmac-drbg.cabal
+++ b/ppad-hmac-drbg.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-hmac-drbg
-version:            0.3.1
+version:            0.3.2
 synopsis:           HMAC-based deterministic random bit generator
 license:            MIT
 license-file:       LICENSE
