Skip to content

Commit ad260c7

Browse files
authored
Merge pull request #782 from IntersectMBO/jdral/blockio-pinned-buffer
`blockio`: guard that buffers are pinned
2 parents dca6656 + c53b697 commit ad260c7

File tree

6 files changed

+179
-24
lines changed

6 files changed

+179
-24
lines changed

blockio/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Revision history for blockio
22

3-
## 0.1.0.0 -- YYYY-mm-dd
3+
## 0.1.0.0 -- 2025-07-09
44

55
* First version. Released on an unsuspecting world.

blockio/src-linux/System/FS/BlockIO/Async.hs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ import Foreign.C.Error
1212
import GHC.IO.Exception
1313
import GHC.Stack
1414
import System.FS.API (BufferOffset (..), FsErrorPath, FsPath,
15-
Handle (..), HasFS (..), SomeHasFS (..), ioToFsError)
15+
Handle (..), HasFS (..), ioToFsError)
1616
import qualified System.FS.BlockIO.API as API
1717
import System.FS.BlockIO.API (IOOp (..), IOResult (..), LockMode,
1818
ioopHandle)
1919
import qualified System.FS.BlockIO.IO.Internal as IOI
2020
import System.FS.IO (HandleIO)
2121
import System.FS.IO.Handle
2222
import qualified System.IO.BlockIO as I
23-
import System.IO.Error (ioeSetErrorString, isResourceVanishedError)
23+
import System.IO.Error (ioeGetErrorType, ioeSetErrorString,
24+
isResourceVanishedError)
2425
import System.Posix.Types
2526

2627
-- | IO instantiation of 'HasBlockIO', using @blockio-uring@.
@@ -64,17 +65,22 @@ submitIO ::
6465
-> IO (VU.Vector IOResult)
6566
submitIO hasFS ioctx ioops = do
6667
ioops' <- mapM ioopConv ioops
67-
ress <- I.submitIO ioctx ioops' `catch` rethrowClosedError
68+
ress <- I.submitIO ioctx ioops' `catch` rethrowFsError
6869
hzipWithM rethrowErrno ioops ress
6970
where
70-
rethrowClosedError :: IOError -> IO a
71-
rethrowClosedError e@IOError{} =
72-
-- Pattern matching on the error is brittle, because the structure of
73-
-- the exception might change between versions of @blockio-uring@.
74-
-- Nonetheless, it's better than nothing.
75-
if isResourceVanishedError e && ioe_location e == "IOCtx closed"
76-
then throwIO (IOI.mkClosedError (SomeHasFS hasFS) "submitIO")
77-
else throwIO e
71+
rethrowFsError :: IOError -> IO a
72+
rethrowFsError e@IOError{}
73+
-- Pattern matching on the error is brittle, because the structure of
74+
-- the exception might change between versions of @blockio-uring@.
75+
-- Nonetheless, it's better than nothing.
76+
| isResourceVanishedError e
77+
, ioe_location e == "IOCtx closed"
78+
= throwIO (IOI.mkClosedError hasFS "submitIO")
79+
| ioeGetErrorType e == InvalidArgument
80+
, ioe_location e == "MutableByteArray is unpinned"
81+
= throwIO (IOI.mkNotPinnedError hasFS "submitIO")
82+
| otherwise
83+
= throwIO e
7884

7985
rethrowErrno ::
8086
HasCallStack

blockio/src/System/FS/BlockIO/API.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ data HasBlockIO m h = HasBlockIO {
8888
--
8989
-- If any of the I\/O operations fails, an 'FsError' exception will be thrown.
9090
--
91+
-- The buffers in the 'IOOp's should be pinned memory. If any buffer is
92+
-- unpinned memory, an 'FsError' exception will be thrown.
93+
--
9194
, submitIO :: HasCallStack => V.Vector (IOOp (PrimState m) h) -> m (VU.Vector IOResult)
9295

9396
-- TODO: once file caching is disabled, subsequent reads/writes with

blockio/src/System/FS/BlockIO/IO/Internal.hs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ module System.FS.BlockIO.IO.Internal (
55
IOCtxParams (..)
66
, defaultIOCtxParams
77
, mkClosedError
8+
, mkNotPinnedError
89
, tryLockFileIO
910
, createHardLinkIO
1011
) where
1112

1213
import Control.DeepSeq (NFData (..))
1314
import Control.Monad.Class.MonadThrow (MonadCatch (bracketOnError),
1415
MonadThrow (..), bracketOnError, try)
15-
import GHC.IO.Exception (IOErrorType (ResourceVanished))
16+
import GHC.IO.Exception
17+
(IOErrorType (InvalidArgument, ResourceVanished))
1618
import qualified GHC.IO.Handle.Lock as GHC
1719
import GHC.Stack (HasCallStack)
1820
import qualified System.FS.API as FS
19-
import System.FS.API (FsError (..), FsPath, HasFS, SomeHasFS (..))
21+
import System.FS.API (FsError (..), FsPath, HasFS)
2022
import System.FS.BlockIO.API (LockFileHandle (..))
2123
import System.FS.IO (HandleIO)
2224
import qualified System.IO as GHC
@@ -54,12 +56,27 @@ defaultIOCtxParams = IOCtxParams {
5456
ioctxConcurrencyLimit = 64 * 3
5557
}
5658

57-
mkClosedError :: HasCallStack => SomeHasFS m -> String -> FsError
58-
mkClosedError (SomeHasFS hasFS) loc = FS.ioToFsError (FS.mkFsErrorPath hasFS (FS.mkFsPath [])) ioerr
59-
where ioerr =
60-
ioeSetErrorString
61-
(mkIOError ResourceVanished loc Nothing Nothing)
62-
("HasBlockIO closed: " <> loc)
59+
{-------------------------------------------------------------------------------
60+
Errors
61+
-------------------------------------------------------------------------------}
62+
63+
mkClosedError :: HasCallStack => HasFS m h -> String -> FsError
64+
mkClosedError hasFS loc =
65+
FS.ioToFsError (FS.mkFsErrorPath hasFS (FS.mkFsPath [])) ioerr
66+
where
67+
ioerr =
68+
ioeSetErrorString
69+
(mkIOError ResourceVanished loc Nothing Nothing)
70+
("HasBlockIO closed: " <> loc)
71+
72+
mkNotPinnedError :: HasCallStack => HasFS m h -> String -> FsError
73+
mkNotPinnedError hasFS loc =
74+
FS.ioToFsError (FS.mkFsErrorPath hasFS (FS.mkFsPath [])) ioerr
75+
where
76+
ioerr =
77+
ioeSetErrorString
78+
(mkIOError InvalidArgument loc Nothing Nothing)
79+
("MutableByteArray is unpinned: " <> loc)
6380

6481
{-------------------------------------------------------------------------------
6582
File locks

blockio/src/System/FS/BlockIO/Serial.hs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Control.Concurrent.Class.MonadMVar
66
import Control.Monad (unless)
77
import Control.Monad.Class.MonadThrow
88
import Control.Monad.Primitive (PrimMonad, PrimState, RealWorld)
9+
import Data.Primitive (MutableByteArray, isMutableByteArrayPinned)
910
import qualified Data.Vector as V
1011
import qualified Data.Vector.Unboxed as VU
1112
import qualified Data.Vector.Unboxed.Mutable as VUM
@@ -59,7 +60,9 @@ data IOCtx m = IOCtx { ctxFS :: SomeHasFS m, openVar :: MVar m Bool }
5960
{-# SPECIALISE guardIsOpen :: IOCtx IO -> IO () #-}
6061
guardIsOpen :: (HasCallStack, MonadMVar m, MonadThrow m) => IOCtx m -> m ()
6162
guardIsOpen ctx = readMVar (openVar ctx) >>= \b ->
62-
unless b $ throwIO (IOI.mkClosedError (ctxFS ctx) "submitIO")
63+
case ctxFS ctx of
64+
SomeHasFS hfs ->
65+
unless b $ throwIO $ IOI.mkClosedError hfs "submitIO"
6366

6467
{-# SPECIALISE initIOCtx :: SomeHasFS IO -> IO (IOCtx IO) #-}
6568
initIOCtx :: MonadMVar m => SomeHasFS m -> m (IOCtx m)
@@ -90,11 +93,24 @@ ioop ::
9093
=> HasFS m h
9194
-> IOOp (PrimState m) h
9295
-> m IOResult
93-
ioop hfs (IOOpRead h off buf bufOff c) =
96+
ioop hfs (IOOpRead h off buf bufOff c) = do
97+
guardPinned hfs buf "submitIO"
9498
IOResult <$> hGetBufExactlyAt hfs h buf bufOff c (fromIntegral off)
95-
ioop hfs (IOOpWrite h off buf bufOff c) =
99+
ioop hfs (IOOpWrite h off buf bufOff c) = do
100+
guardPinned hfs buf "submitIO"
96101
IOResult <$> hPutBufExactlyAt hfs h buf bufOff c (fromIntegral off)
97102

103+
{-# SPECIALISE guardPinned :: HasFS IO h -> MutableByteArray RealWorld -> String -> IO () #-}
104+
guardPinned ::
105+
MonadThrow m
106+
=> HasFS m h
107+
-> MutableByteArray (PrimState m)
108+
-> String
109+
-> m ()
110+
guardPinned hfs buf loc =
111+
unless (isMutableByteArrayPinned buf) $
112+
throwIO (IOI.mkNotPinnedError hfs loc)
113+
98114
{-# SPECIALISE hmapM ::
99115
VUM.Unbox b
100116
=> (a -> IO b)

blockio/test/Main.hs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,24 @@ module Main (main) where
55
import Control.Concurrent (modifyMVar_, newMVar, threadDelay,
66
withMVar)
77
import Control.Concurrent.Async
8-
import Control.Exception (SomeException (SomeException), bracket, try)
8+
import Control.Exception (Exception (..),
9+
SomeException (SomeException), bracket, try)
910
import Control.Monad
1011
import Control.Monad.Primitive
1112
import Data.ByteString (ByteString)
1213
import qualified Data.ByteString as BS
1314
import qualified Data.ByteString.Char8 as BSC
15+
import qualified Data.ByteString.Lazy as LBS
1416
import Data.Foldable (traverse_)
1517
import Data.Functor.Compose (Compose (Compose))
18+
import qualified Data.List as List
1619
import Data.Maybe (catMaybes)
1720
import Data.Primitive.ByteArray
1821
import Data.Typeable
1922
import qualified Data.Vector as V
2023
import qualified Data.Vector.Unboxed as VU
2124
import System.FS.API
25+
import qualified System.FS.API.Lazy as FS
2226
import qualified System.FS.API.Strict as FS
2327
import System.FS.API.Strict (hPutAllStrict)
2428
import qualified System.FS.BlockIO.API as FS
@@ -40,7 +44,18 @@ tests = testGroup "blockio:test" [
4044
, testCase "example_closeIsIdempotent" example_closeIsIdempotent
4145
, testProperty "prop_readWrite" prop_readWrite
4246
, testProperty "prop_submitToClosedCtx" prop_submitToClosedCtx
47+
48+
-- Context
49+
, testProperty "prop_submitIO_contextClosed" prop_submitIO_contextClosed
50+
51+
-- Pinned vs. unpinned buffers
52+
, testProperty "prop_submitIO_buffersPinned" prop_submitIO_buffersPinned
53+
, testProperty "prop_submitIO_buffersUnpinned" prop_submitIO_buffersUnpinned
54+
55+
-- File locks
4356
, testProperty "prop_tryLockFileExclusiveTwice" prop_tryLockFileExclusiveTwice
57+
58+
-- Storage synchronisation
4459
, testProperty "prop_synchronise" prop_synchronise
4560
, testProperty "prop_synchroniseFile_fileDoesNotExist"
4661
prop_synchroniseFile_fileDoesNotExist
@@ -118,6 +133,104 @@ prop_submitToClosedCtx bs = ioProperty $ withSystemTempDirectory "prop_a" $ \dir
118133
Left _ -> Just $ tabulate "submitIO successful" [show False] $ counterexample "expected failure, but got success" (b === True)
119134
Right _ -> Just $ tabulate "submitIO successful" [show True] $ counterexample "expected success, but got failure" (b === False)
120135

136+
{-------------------------------------------------------------------------------
137+
Closed context
138+
-------------------------------------------------------------------------------}
139+
140+
-- | Test that 'submitIO' on a closed context returns a "context closed" error
141+
prop_submitIO_contextClosed :: Property
142+
prop_submitIO_contextClosed =
143+
ioProperty $
144+
withTempIOHasBlockIO "prop_submitIO_unpinnedBuffers" $ \hfs hbio ->
145+
FS.withFile hfs path (FS.ReadWriteMode FS.MustBeNew) $ \h -> do
146+
void $ FS.hPutAll hfs h $ LBS.pack [1..100]
147+
buf <- newByteArray 17
148+
let ioops = V.fromList [
149+
IOOpWrite h 0 buf 0 17
150+
, IOOpRead h 0 buf 0 17
151+
]
152+
close hbio
153+
eith <- try @FsError $ submitIO hbio ioops
154+
pure $ case eith of
155+
Left e
156+
| isClosedError e
157+
-> property True
158+
| otherwise
159+
-> counterexample ("Unexpected error: " <> displayException e) False
160+
Right _
161+
-> counterexample ("Unexpected success") False
162+
where
163+
path = FS.mkFsPath ["temp-file"]
164+
165+
-- TODO: add a property that checks @isClosedError . mkClosedError = True@
166+
isClosedError :: FsError -> Bool
167+
isClosedError e
168+
-- TODO: add an FsResourceVanished constructor to FsErrorType?
169+
| fsErrorType e == FsOther
170+
, "HasBlockIO closed: " `List.isPrefixOf` (fsErrorString e)
171+
= True
172+
| otherwise
173+
= False
174+
175+
{-------------------------------------------------------------------------------
176+
Pinned vs. unpinned buffers
177+
-------------------------------------------------------------------------------}
178+
179+
-- | Test that 'submitIO' using pinned buffers returns /no/ "unpinned buffers"
180+
-- error
181+
prop_submitIO_buffersPinned :: Property
182+
prop_submitIO_buffersPinned =
183+
ioProperty $
184+
withTempIOHasBlockIO "prop_submitIO_pinnedBuffers" $ \hfs hbio ->
185+
FS.withFile hfs path (FS.ReadWriteMode FS.MustBeNew) $ \h -> do
186+
void $ FS.hPutAll hfs h $ LBS.pack [1..100]
187+
buf <- newPinnedByteArray 17
188+
let ioops = V.fromList [
189+
IOOpWrite h 0 buf 0 17
190+
, IOOpRead h 0 buf 0 17
191+
]
192+
eith <- try @FsError $ submitIO hbio ioops
193+
pure $ case eith of
194+
Left e
195+
-> counterexample ("Unexpected error: " <> displayException e) False
196+
Right _
197+
-> property True
198+
where
199+
path = FS.mkFsPath ["temp-file"]
200+
201+
-- | Test that 'submitIO' using unpinned buffers returns an "unpinned buffers" error
202+
prop_submitIO_buffersUnpinned :: Property
203+
prop_submitIO_buffersUnpinned =
204+
ioProperty $
205+
withTempIOHasBlockIO "prop_submitIO_unpinnedBuffers" $ \hfs hbio ->
206+
FS.withFile hfs path (FS.ReadWriteMode FS.MustBeNew) $ \h -> do
207+
void $ FS.hPutAll hfs h $ LBS.pack [1..100]
208+
buf <- newByteArray 17
209+
let ioops = V.fromList [
210+
IOOpWrite h 0 buf 0 17
211+
, IOOpRead h 0 buf 0 17
212+
]
213+
eith <- try @FsError $ submitIO hbio ioops
214+
pure $ case eith of
215+
Left e
216+
| isNotPinnedError e
217+
-> property True
218+
| otherwise
219+
-> counterexample ("Unexpected error: " <> displayException e) False
220+
Right _
221+
-> counterexample ("Unexpected success") False
222+
where
223+
path = FS.mkFsPath ["temp-file"]
224+
225+
-- TODO: add a property that checks @isNotPinnedError . mkNotPinnedError = True@
226+
isNotPinnedError :: FsError -> Bool
227+
isNotPinnedError e
228+
| fsErrorType e == FsInvalidArgument
229+
, "MutableByteArray is unpinned: " `List.isPrefixOf` (fsErrorString e)
230+
= True
231+
| otherwise
232+
= False
233+
121234
{-------------------------------------------------------------------------------
122235
File locks
123236
-------------------------------------------------------------------------------}

0 commit comments

Comments
 (0)