Skip to content

Commit 5ff1926

Browse files
hasufellBodigrim
authored andcommitted
Ensure that FilePaths don't contain interior NULs
Follow: * https://gitlab.haskell.org/ghc/ghc/-/merge_requests/10110 * haskell/core-libraries-committee#144
1 parent 48d590c commit 5ff1926

File tree

4 files changed

+126
-4
lines changed

4 files changed

+126
-4
lines changed

System/Posix/ByteString/FilePath.hsc

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{-# LANGUAGE Safe #-}
1+
{-# LANGUAGE Trustworthy #-}
22
{-# LANGUAGE TypeApplications #-}
33

44
-----------------------------------------------------------------------------
@@ -41,8 +41,10 @@ import Foreign.C hiding (
4141

4242
import Control.Monad
4343
import Control.Exception
44+
import Data.ByteString.Internal (c_strlen)
4445
import GHC.Foreign as GHC ( peekCStringLen )
4546
import GHC.IO.Encoding ( getFileSystemEncoding )
47+
import GHC.IO.Exception
4648
import Data.ByteString as B
4749
import Data.ByteString.Char8 as BC
4850
import Prelude hiding (FilePath)
@@ -54,7 +56,7 @@ import Data.Monoid ((<>))
5456
type RawFilePath = ByteString
5557

5658
withFilePath :: RawFilePath -> (CString -> IO a) -> IO a
57-
withFilePath = useAsCString
59+
withFilePath path = useAsCStringSafe path
5860

5961
peekFilePath :: CString -> IO RawFilePath
6062
peekFilePath = packCString
@@ -147,3 +149,24 @@ decodeWithBasePosix ba = B.useAsCStringLen ba $ \fp -> peekFilePathPosix fp
147149
where
148150
peekFilePathPosix :: CStringLen -> IO String
149151
peekFilePathPosix fp = getFileSystemEncoding >>= \enc -> GHC.peekCStringLen enc fp
152+
153+
-- | Wrapper around 'useAsCString', checking the encoded 'FilePath' for internal NUL octets as these are
154+
-- disallowed in POSIX filepaths. See https://gitlab.haskell.org/ghc/ghc/-/issues/13660
155+
useAsCStringSafe :: RawFilePath -> (CString -> IO a) -> IO a
156+
useAsCStringSafe path f = useAsCStringLen path $ \(ptr, len) -> do
157+
clen <- c_strlen ptr
158+
if clen == fromIntegral len
159+
then f ptr
160+
else do
161+
path' <- either (const (BC.unpack path)) id <$> try @IOException (decodeWithBasePosix path)
162+
ioError (err path')
163+
where
164+
err path' =
165+
IOError
166+
{ ioe_handle = Nothing
167+
, ioe_type = InvalidArgument
168+
, ioe_location = "checkForInteriorNuls"
169+
, ioe_description = "POSIX filepaths must not contain internal NUL octets."
170+
, ioe_errno = Nothing
171+
, ioe_filename = Just path'
172+
}

System/Posix/PosixPath/FilePath.hsc

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# LANGUAGE CPP #-}
22
{-# LANGUAGE TypeApplications #-}
3+
{-# LANGUAGE PatternSynonyms #-}
34

45
-----------------------------------------------------------------------------
56
-- |
@@ -40,20 +41,22 @@ import Foreign.C hiding (
4041
throwErrnoPathIfMinus1_ )
4142

4243
import System.OsPath.Types
44+
import Data.ByteString.Internal (c_strlen)
4345
import Control.Monad
4446
import Control.Exception
4547
import System.OsPath.Posix as PS
4648
import System.OsPath.Data.ByteString.Short
4749
import Prelude hiding (FilePath)
48-
import System.OsString.Internal.Types (PosixString(..))
50+
import System.OsString.Internal.Types (PosixString(..), pattern PS)
51+
import GHC.IO.Exception
4952

5053
#if !MIN_VERSION_base(4, 11, 0)
5154
import Data.Monoid ((<>))
5255
#endif
5356

5457

5558
withFilePath :: PosixPath -> (CString -> IO a) -> IO a
56-
withFilePath = useAsCString . getPosixString
59+
withFilePath path = useAsCStringSafe path
5760

5861
peekFilePath :: CString -> IO PosixPath
5962
peekFilePath = fmap PosixString . packCString
@@ -140,3 +143,24 @@ throwErrnoTwoPathsIfMinus1_ loc path1 path2 action = do
140143

141144
_toStr :: PosixPath -> String
142145
_toStr = fmap PS.toChar . PS.unpack
146+
147+
-- | Wrapper around 'useAsCString', checking the encoded 'FilePath' for internal NUL octets as these are
148+
-- disallowed in POSIX filepaths. See https://gitlab.haskell.org/ghc/ghc/-/issues/13660
149+
useAsCStringSafe :: PosixPath -> (CString -> IO a) -> IO a
150+
useAsCStringSafe pp@(PS path) f = useAsCStringLen path $ \(ptr, len) -> do
151+
clen <- c_strlen ptr
152+
if clen == fromIntegral len
153+
then f ptr
154+
else do
155+
path' <- either (const (_toStr pp)) id <$> try @IOException (PS.decodeFS pp)
156+
ioError (err path')
157+
where
158+
err path' =
159+
IOError
160+
{ ioe_handle = Nothing
161+
, ioe_type = InvalidArgument
162+
, ioe_location = "checkForInteriorNuls"
163+
, ioe_description = "POSIX filepaths must not contain internal NUL octets."
164+
, ioe_errno = Nothing
165+
, ioe_filename = Just path'
166+
}

tests/T13660.hs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
4+
module Main where
5+
6+
import Data.Maybe
7+
#if !MIN_VERSION_base(4, 11, 0)
8+
import Data.Monoid ((<>))
9+
#endif
10+
import GHC.IO.Exception
11+
import System.IO.Error
12+
import System.OsPath.Posix
13+
import System.OsString.Internal.Types (PosixString(..))
14+
import System.Posix.IO (defaultFileFlags, OpenFileFlags(..), OpenMode(..))
15+
import System.Posix.ByteString.FilePath
16+
17+
import qualified Data.ByteString.Char8 as C
18+
import qualified System.OsPath.Data.ByteString.Short as SBS
19+
import qualified System.Posix.Env.PosixString as PS
20+
import qualified System.Posix.IO.PosixString as PS
21+
import qualified System.Posix.IO.ByteString as BS
22+
import qualified System.Posix.Env.ByteString as BS
23+
24+
25+
main :: IO ()
26+
main = do
27+
tmp <- getTemporaryDirectory
28+
let fp = tmp <> fromStr' "/hello\0world"
29+
res <- tryIOError $ PS.openFd fp WriteOnly df
30+
31+
tmp' <- getTemporaryDirectory'
32+
let fp' = tmp' <> "/hello\0world"
33+
res' <- tryIOError $ BS.openFd fp' WriteOnly df
34+
35+
case (res, res') of
36+
(Left e, Left e')
37+
| e == fileError (_toStr fp)
38+
, e' == fileError (C.unpack fp') -> pure ()
39+
| otherwise -> fail $ "Unexpected errors: " <> show e <> "\n\t" <> show e'
40+
(Right _, Left _) -> fail "System.Posix.IO.PosixString.openFd should not accept filepaths with NUL bytes"
41+
(Left _, Right _) -> fail "System.Posix.IO.ByteString.openFd should not accept filepaths with NUL bytes"
42+
(Right _, Right _) -> fail $ "System.Posix.IO.PosixString.openFd and System.Posix.IO.ByteString.openFd" <>
43+
" should not accept filepaths with NUL bytes"
44+
45+
where
46+
df :: OpenFileFlags
47+
df = defaultFileFlags{ trunc = True, creat = Just 0o666, noctty = True, nonBlock = True }
48+
49+
getTemporaryDirectory :: IO PosixPath
50+
getTemporaryDirectory = fromMaybe (fromStr' "/tmp") <$> PS.getEnv (fromStr' "TMPDIR")
51+
52+
getTemporaryDirectory' :: IO RawFilePath
53+
getTemporaryDirectory' = fromMaybe "/tmp" <$> BS.getEnv "TMPDIR"
54+
55+
fromStr' = pack . fmap unsafeFromChar
56+
57+
_toStr (PosixString sbs) = C.unpack $ SBS.fromShort sbs
58+
59+
fileError fp = IOError
60+
{ ioe_handle = Nothing
61+
, ioe_type = InvalidArgument
62+
, ioe_location = "checkForInteriorNuls"
63+
, ioe_description = "POSIX filepaths must not contain internal NUL octets."
64+
, ioe_errno = Nothing
65+
, ioe_filename = Just fp
66+
}
67+

unix.cabal

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,11 @@ test-suite SemaphoreInterrupt
349349
default-language: Haskell2010
350350
build-depends: base, unix
351351
ghc-options: -Wall -threaded
352+
353+
test-suite T13660
354+
hs-source-dirs: tests
355+
main-is: T13660.hs
356+
type: exitcode-stdio-1.0
357+
default-language: Haskell2010
358+
build-depends: base, unix, filepath >= 1.4.100.0 && < 1.5, bytestring
359+
ghc-options: -Wall

0 commit comments

Comments
 (0)