Skip to content

Commit 1b0536f

Browse files
adamgundryBodigrim
authored andcommitted
Builder: avoid unsound buffer reuse (#690) (#691)
`toLazyByteString :: Builder -> LazyByteString` had a race condition that could generate wrong results if two threads concurrently evaluated the result. This bug was introduced in #581 (5c4d236) and first present in release 0.11.5.0 (as 0c030bb). Due to the use of `unsafeDupablePerformIO` for performance, it is critical that any IO actions executed when running a `Builder` can be interrupted or executed multiple times. In principle, filling a buffer is safe provided the buffer is used only once and the same bytes are written each time. However, `wrapChunk` in `buildStepToCIOS` would re-use a buffer in the trimming case after copying its contents to produce a new trimmed chunk. This is safe when run in a single thread, but if two threads simultaneously execute the code, one of them may still be copying the contents while the other starts overwriting the buffer. This patch fixes `wrapChunk` to unconditionally allocate a new buffer after trimming, rather than re-using the old buffer. This will presumably come at a slight performance cost for builders inserting many trimmed chunks.
1 parent 8f199e3 commit 1b0536f

File tree

1 file changed

+4
-4
lines changed

1 file changed

+4
-4
lines changed

Data/ByteString/Builder/Internal.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,7 @@ buildStepToCIOS (AllocationStrategy nextBuffer bufSize trim) =
11961196
-- Checking for empty case avoids allocating 'n-1' empty
11971197
-- buffers for 'n' insertChunkH right after each other.
11981198
if isEmpty
1199-
then fill nextStep (Buffer fpbuf (BufferRange pbuf pe))
1199+
then fill nextStep buf
12001200
else do buf' <- nextBuffer (Just (buf, bufSize))
12011201
fill nextStep buf'
12021202

@@ -1208,9 +1208,9 @@ buildStepToCIOS (AllocationStrategy nextBuffer bufSize trim) =
12081208
| trim chunkSize size = do
12091209
bs <- S.createFp chunkSize $ \fpbuf' ->
12101210
S.memcpyFp fpbuf' fpbuf chunkSize
1211-
-- Instead of allocating a new buffer after trimming,
1212-
-- we re-use the old buffer and consider it empty.
1213-
return $ Yield1 bs (mkCIOS True)
1211+
-- It is not safe to re-use the old buffer (see #690),
1212+
-- so we allocate a new buffer after trimming.
1213+
return $ Yield1 bs (mkCIOS False)
12141214
| otherwise =
12151215
return $ Yield1 (S.BS fpbuf chunkSize) (mkCIOS False)
12161216
where

0 commit comments

Comments
 (0)