Skip to content

unfoldrN, unfoldrNM and fromListN are dangerous #301

Description

@lehins

Size argument supplied to all three functions unfoldrN, unfoldrNM and fromListN serves as hint for upper bound for the length of a vector. The problem is that regardless of how big a vector would have been, the supplied upper bound memory is allocated, even if it too big, which can result in an application being killed with an asynchronous exception HeapOverflow

fromListN.hs:

import qualified Data.Vector.Primitive as V

main = do
  let xs = [1, 2, 3, 4, 5] :: [Int]
  print $ V.fromListN (maxBound `div` 8) xs
$ ghc fromListN.hs && ./fromListN
[1 of 1] Compiling Main             ( fromListN.hs, fromListN.o )
fromListN: Out of memory

unfoldrN.hs

import qualified Data.Vector.Primitive as V

main = print (V.unfoldrN (maxBound `div` 8) (const Nothing) () :: V.Vector Int)
$ ghc unfoldrN.hs && ./unfoldrN
[1 of 1] Compiling Main             ( unfoldrN.hs, unfoldrN.o )
Linking unfoldrN ...
unfoldrN: Out of memory

unfoldrNM.hs

import Control.Exception
import qualified Data.Vector.Primitive as V

main = do
  eRes <- try (V.unfoldrNM (maxBound `div` 8) (const $ pure Nothing) () :: IO (V.Vector Int))
  print (eRes :: Either SomeException (V.Vector Int))
$ stack exec -- ghc unfoldrNM.hs -O2 && ./unfoldrNM
[1 of 1] Compiling Main             ( unfoldrNM.hs, unfoldrNM.o )
Linking unfoldrNM ...
Left heap overflow

Activity

  1. changed the title [-]unfoldrN, unfoldrNM and fromListN are dangerous[/-] [+]unfoldrN, unfoldrNM and fromListN are *unreasonable* with respect to their size parameters[/+] on Feb 28, 2020
  2. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor

    dangerous might be the wrong word here, but I agree with the intent (and please forgive my edit).

    lets think about when/how we might want it to fail/behave instead!
    if end users of an application can tickle this, thats certainly a denial of service (for at least the thread doing the calculation).

    i actually think its probably very reasonable for apis to throw an exception when too much memory is requested. (at least if thats not exposed in the normal api results).

    But you do raise a really important point, size here should perhaps be treated as a combo of both a hint and lint!

    I dont think theres a trivial answer here as such. since you CAN (with extreme care) have off heap memory mapped arrays that take up terabytes of virtual/physical memory. I think physical memory limits on most platforms are ~48 bits of addressable bytes, but I believe thats orthogonal to cpu architecture related virtual memory limits?

  3. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor

    to be clear: i dont view this as a security problem as such, at that rate, Eq and Ord on vectors are security problems :)

  4. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor

    @lehins would you prefer that it errors instead because the list doesn't match the provided size (with one of those amortized doubling schemes + a force/deep copy at the end?)

  5. lehins commented on Feb 28, 2020

    @lehins
    ContributorAuthor

    Dangerous was a very good word here. An application normally should not recover from AsyncExceptions, so it can be viewed as security concern.

    There is no need to throw any errors all three functions, since they are well behaved. I think the easiest solution would be to set the size to Unknown and that would handle the problem for all three them.

  6. lehins commented on Feb 28, 2020

    @lehins
    ContributorAuthor

    I mean replacing Max with an Unknown

  7. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor
  8. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor
  9. lehins commented on Feb 28, 2020

    @lehins
    ContributorAuthor

    I looked at the code :)

    Out of curiosity how’d you hit this sharp edge ?

  10. lehins commented on Feb 28, 2020

    @lehins
    ContributorAuthor

    Not exactly. slice can fail by definition. fromListN on the other hand should never fail

    That particular change sounds analogous to the fusible slice vs exceptions
    issue we discussed before.

  11. lehins commented on Feb 28, 2020

    @lehins
    ContributorAuthor

    This is not entirely true, at least for a correct implementation of concurrency. Exception should be rethrown in the main thread:

    thats certainly a denial of service (for at least the thread doing the calculation).

    module Main where
    
    import Control.Concurrent
    import Control.Concurrent.Async
    import Control.Exception
    import qualified Data.Vector.Primitive as V
    
    main = do
      eRes <- try $ concurrently_
        (print =<< (V.unfoldrNM (maxBound `div` 8) (const $ pure Nothing) () :: IO (V.Vector Int)))
        (print "foo" >> threadDelay 1000000 >> print "bar")
      print (eRes :: Either AsyncException ())
    $ ghc unfoldrn.hs -O2 && ./unfoldrn
    [1 of 1] Compiling Main             ( unfoldrn.hs, unfoldrn.o )
    Linking unfoldrn ...
    "foo"
    Left heap overflow
  12. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor
  13. lehins commented on Feb 28, 2020

    @lehins
    ContributorAuthor

    I don't think I follow what you are suggesting.

    All three functions take the size argument as the upper bound, it never has to be exact, that's the whole point of the unfoldrN[M] functions.

    As far as fromListN is concerned semantics are clear from documentation that it is also assumed to be upper bound:

    vector/Data/Vector.hs

    Lines 1709 to 1716 in eeb42ad

    -- | /O(n)/ Convert the first @n@ elements of a list to a vector
    --
    -- @
    -- fromListN n xs = 'fromList' ('take' n xs)
    -- @
    fromListN :: Int -> [a] -> Vector a
    {-# INLINE fromListN #-}
    fromListN = G.fromListN

    Implementation of traversable can continue using the dangerous version, because we know the source vector has correct size

    Because if you look at some of
    the patches motivated by folks using vector with compact heap code,
    fromListN is used with the Exact semantic in a few operations. I think
    traverse for boxed vector is one example.

  14. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor
  15. lehins commented on Feb 28, 2020

    @lehins
    ContributorAuthor

    Note that current semantics in vector for fromListN do not follow other libraries, so it could as well be a bug, but I personally don't really care which way we go. Especially since IsList documentation states:

    If the given hint does not equal to the input list's length the behaviour of fromListN is not specified.

    Other data structures that have IsList instance (eg, List, NonEmpty, Set) take the size argument as a hint and it has no affect the resulting data structure. But since vector used it as an upper bound for the longest time, it might not make sense to switch the semantics now

  16. 2 remaining items

  17. cartazio commented on Feb 28, 2020

    @cartazio
    Contributor
  18. cartazio commented on Mar 12, 2020

    @cartazio
    Contributor

    https://gist.github.com/cartazio/517752ce92b3859a5b86fc396b404f6b (i did a rg fromListN -t haskell over all of current hackage)
    using an old run of cabal list --simple | awk '{ print $1 }' | uniq | xargs -P12 -n1 cabal get from feb 2019
    (which gives us all the extent packages that uses fromListN).

    i'll look though it and a more recent run i'm preparing.

    and then combine that with the positions and perspectives articulated on the library list to make a determinition about this :)

  19. added this to the 0.13 milestone on Jun 11, 2020
  20. changed the title [-]unfoldrN, unfoldrNM and fromListN are *unreasonable* with respect to their size parameters[/-] [+]unfoldrN, unfoldrNM and fromListN are dangerous[/+] on Jan 16, 2021
  21. Shimuuar commented on Apr 18, 2021

    @Shimuuar
    Contributor

    I think that changing type hint from Max to Unknown for unfoldrN/unfoldrNM is right thing to do. In addition to possible heap overflow it's poor strategy in cases when upper limit is used as some sort of safeguard and vector is usually much smaller. It just wastes memory.

    On other hand such change for fromListN makes no sense. It's mostly an optimization for cases when vector size is known in advance. It allows avoid reallocation of buffers when growing vector. Yes. it allows to induce heap overflow if size if hands of attacker. Without such preallocation we could just drop fromListN or implement it as fromList . take n. I think it's better to leave function as it is and just update documentation explaining possible dangers

  22. added a commit that references this issue on May 1, 2021
    fd98c93
  23. gksato commented on May 21, 2021

    @gksato
    Contributor

    This may be off-topic, but looking through this conversation, I wish the definition of Size were

    data Size = Exact Int
              | DoublingMax { preAlloc :: Int, max :: Int }
              | DoublingUnknown { preAlloc :: Int }

    Who would imagine that prefixing take n to a vector would enlarge the possibility of DoS just because n came from the outer world? Who would imagine that the more strict constraint to the size would produce more DoS-prone code?

  24. gksato commented on May 21, 2021

    @gksato
    Contributor

    This style of definition of Size is the exact copy of Rust Iterator: in that language we have

    trait Iterator {
    
        ...
    
        fn size_hint(&self) -> (usize, Option<usize>)
    }
    unsafe trait TrustedLen: Iterator {}
  25. Shimuuar commented on May 21, 2021

    @Shimuuar
    Contributor

    This may be off-topic, but looking through this conversation, I wish the definition of Size were

    I think it's good idea. It allows to specify allocation strategy more precisely

  26. gksato commented on May 21, 2021

    @gksato
    Contributor

    specify allocation strategy more precisely

    Yes. Since preAlloc only affects time and memory consumption and not memory safety nor visible result, we could even provide a combinator that manipulate preAlloc in Vector.x (x=Generic, Unboxed, etc) modules. We could just default preAlloc to the minimum possible size.

  27. added a commit that references this issue on May 23, 2022
    9281d67
  28. added a commit that references this issue on Oct 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions