Skip to content

Segmentation fault from unsafeFreezeArray#

Summary

Using functions that ultimately desugar into unsafeFreezeArray# without doing copy beforehand leads to crash.

Steps to reproduce

Clone https://github.com/sergv/array-freeze-bug and do cabal run array-freeze. If recent enough nix with flakes support is available then do nix develop -c cabal run array-freeze.

Otherwise consider program:

#!/usr/bin/env cabal
{- cabal:
build-depends:
  , base
  , primitive
  , vector
-}

{-# LANGUAGE BangPatterns        #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE MagicHash           #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main (main) where

import Control.Exception
import Control.Monad.Primitive
import Data.Char
import Data.List qualified as L
import Data.Traversable
import Data.Vector qualified as V
import Data.Vector.Mutable qualified as VM
import Data.Vector.Unboxed qualified as U

{-# NOINLINE process #-}
process
  :: V.MVector (PrimState IO) (U.Vector Int)
  -> [U.Vector Int]
  -> V.Vector Int
  -> IO Int
process store haystacks needle = do
  fmap L.minimum $ for haystacks $ \haystack -> do
    V.iforM_ needle $ \idx i -> do
      VM.write store idx (U.slice i 5 haystack)

    -- -- GOOD
    -- store' <- V.freeze store

    -- BAD
    store' <- V.unsafeFreeze store

    evaluate $ processOne store' 0

{-# NOINLINE processOne #-}
processOne
  :: V.Vector (U.Vector Int)
  -> Int
  -> Int
processOne !items !acc
  | V.null items
  = acc
  | otherwise
  = let !curr = V.head items
    in processOne (V.tail items) $! (acc `max` U.head curr + 1)

main :: IO ()
main = do

  let n :: Int
      n = 10000

  let haystacks :: [U.Vector Int]
      haystacks =
        map (U.fromList . map ord) $ map (\k -> "abcdefghijklmnopqrstuvwxyz" ++ show k) [1..n]

      needle :: V.Vector Int
      needle = V.fromList [1..10]

  !res <- do
    store <- VM.new (V.length needle)
    process store haystacks needle

  putStrLn $ "res = " ++ show res

  pure ()

It can either be built with cabal file present in the git repository or run as standalone cabal script like this: cabal ./Main.hs.

Expected behavior

In any of outlined scenarios of running the reproduction example the expectation is that program prints a number.

What actually happens is that the program ends with segmentation fault.

Discussion

If unsafeFreeze in replaced by vanilla freeze then program works and prints 108. The intention is to avoid copies by employing unsafeFreeze onto reusable mutable store that gets allocated only once outside of a processing loop.

For convenience the git repsitory includes results of dumping core on GHC 9.4.2. The version that fails: https://github.com/sergv/array-freeze-bug/blob/master/Main.bad.dump-simpl and the version that works: https://github.com/sergv/array-freeze-bug/blob/master/Main.good.dump-simpl. The difference between the two (https://github.com/sergv/array-freeze-bug/blob/master/Main.dump-simpl.diff) comes to calling unsafeArrayFreeze# vs doing a copy.

Extensive investigation in a bigger program where this problem initially occured suggests that argument of processOne becomes invalid after some iterations and at one point just contains junk vectors (maybe due to GC, haven't conclusively established that).

Perhaps I'm just missing something about unsafeFrezeArray#'s semantics but its docs are pretty sparse and it seems its return value shoud be valid as long as its argument is kept around.

Environment

  • GHC version used: 9.4.2, 9.2.4, 9.0.2, 8.10.7

Optional:

  • Operating System: NixOS 22.05
  • System Architecture: x86_64
Edited by Sergey Vinokurov
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information