Skip to content

Commit 98242d4

Browse files
erikdangerman
authored andcommitted
Add pre and post build hooks
Run a program (named "preBuildHook") before doing a package build and another program (named "postBuildHook") after the package is built. The exit code from the pre-build hook is passed to the post-build hook. The commit includes documentation for the hooks and the security safeguards implemented to avoid the running of malicious hook files. (cherry picked from commit 5f7b47f)
1 parent 252f5cb commit 98242d4

File tree

17 files changed

+315
-3
lines changed

17 files changed

+315
-3
lines changed

cabal-install/cabal-install.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ library
130130
Distribution.Client.GlobalFlags
131131
Distribution.Client.Haddock
132132
Distribution.Client.HashValue
133+
Distribution.Client.HookAccept
133134
Distribution.Client.HttpUtils
134135
Distribution.Client.IndexUtils
135136
Distribution.Client.IndexUtils.ActiveRepos

cabal-install/src/Distribution/Client/CmdFreeze.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ freezeAction flags@NixStyleFlags{..} extraArgs globalFlags = do
142142
(_, elaboratedPlan, _, totalIndexState, activeRepos) <-
143143
rebuildInstallPlan
144144
verbosity
145+
mempty
145146
distDirLayout
146147
cabalDirLayout
147148
projectConfig

cabal-install/src/Distribution/Client/Errors.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ data CabalInstallException
186186
| MissingPackageList RemoteRepo
187187
| CmdPathAcceptsNoTargets
188188
| CmdPathCommandDoesn'tSupportDryRun
189+
| HookAcceptUnknown FilePath FilePath String
190+
| HookAcceptHashMismatch FilePath FilePath String String
189191
deriving (Show, Typeable)
190192

191193
exceptionCodeCabalInstall :: CabalInstallException -> Int
@@ -338,6 +340,8 @@ exceptionCodeCabalInstall e = case e of
338340
MissingPackageList{} -> 7160
339341
CmdPathAcceptsNoTargets{} -> 7161
340342
CmdPathCommandDoesn'tSupportDryRun -> 7163
343+
HookAcceptUnknown{} -> 7164
344+
HookAcceptHashMismatch{} -> 7165
341345

342346
exceptionMessageCabalInstall :: CabalInstallException -> String
343347
exceptionMessageCabalInstall e = case e of
@@ -857,6 +861,36 @@ exceptionMessageCabalInstall e = case e of
857861
"The 'path' command accepts no target arguments."
858862
CmdPathCommandDoesn'tSupportDryRun ->
859863
"The 'path' command doesn't support the flag '--dry-run'."
864+
HookAcceptUnknown hsPath fpath hash ->
865+
concat
866+
[ "The following file does not appear in the hooks-security file.\n"
867+
, " hook file : "
868+
, fpath
869+
, "\n"
870+
, " file hash : "
871+
, hash
872+
, "\n"
873+
, "After checking the contents of that file, it should be added to the\n"
874+
, "hooks-security file with either AcceptAlways or better yet an AcceptHash.\n"
875+
, "The hooks-security file is (probably) located at: "
876+
, hsPath
877+
]
878+
HookAcceptHashMismatch hsPath fpath expected actual ->
879+
concat
880+
[ "\nHook file hash mismatch for:\n"
881+
, " hook file : "
882+
, fpath
883+
, "\n"
884+
, " expected hash: "
885+
, expected
886+
, "\n"
887+
, " actual hash : "
888+
, actual
889+
, "\n"
890+
, "The hook file should be inspected and if deemed ok, the hooks-security file updated.\n"
891+
, "The hooks-security file is (probably) located at: "
892+
, hsPath
893+
]
860894

861895
instance Exception (VerboseException CabalInstallException) where
862896
displayException :: VerboseException CabalInstallException -> [Char]

cabal-install/src/Distribution/Client/HashValue.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
module Distribution.Client.HashValue
66
( HashValue
77
, hashValue
8+
, hashValueFromHex
89
, truncateHash
910
, showHashValue
1011
, readFileHashValue
@@ -52,6 +53,11 @@ instance Structured HashValue
5253
hashValue :: LBS.ByteString -> HashValue
5354
hashValue = HashValue . SHA256.hashlazy
5455

56+
-- From a base16 encoded Bytestring to a HashValue with `Base16`'s
57+
-- error passing through.
58+
hashValueFromHex :: BS.ByteString -> Either String HashValue
59+
hashValueFromHex bs = HashValue <$> Base16.decode bs
60+
5561
showHashValue :: HashValue -> String
5662
showHashValue (HashValue digest) = BS.unpack (Base16.encode digest)
5763

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{-# LANGUAGE DeriveGeneric #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
4+
module Distribution.Client.HookAccept
5+
( HookAccept (..)
6+
, assertHookHash
7+
, loadHookHasheshMap
8+
, parseHooks
9+
) where
10+
11+
import Distribution.Client.Compat.Prelude
12+
13+
import Data.ByteString.Char8 (ByteString)
14+
import qualified Data.ByteString.Char8 as BS
15+
16+
import qualified Data.Map.Strict as Map
17+
18+
import Distribution.Client.Config (getConfigFilePath)
19+
import Distribution.Client.Errors (CabalInstallException (..))
20+
import Distribution.Client.HashValue (HashValue, hashValueFromHex, readFileHashValue, showHashValue)
21+
import Distribution.Simple.Setup (Flag (..))
22+
import Distribution.Simple.Utils (dieWithException)
23+
import Distribution.Verbosity (normal)
24+
25+
import System.FilePath (takeDirectory, (</>))
26+
27+
data HookAccept
28+
= AcceptAlways
29+
| AcceptHash HashValue
30+
deriving (Eq, Show, Generic)
31+
32+
instance Monoid HookAccept where
33+
mempty = AcceptAlways -- Should never be needed.
34+
mappend = (<>)
35+
36+
instance Semigroup HookAccept where
37+
AcceptAlways <> AcceptAlways = AcceptAlways
38+
AcceptAlways <> AcceptHash h = AcceptHash h
39+
AcceptHash h <> AcceptAlways = AcceptHash h
40+
AcceptHash h <> _ = AcceptHash h
41+
42+
instance Binary HookAccept
43+
instance Structured HookAccept
44+
45+
assertHookHash :: Map FilePath HookAccept -> FilePath -> IO ()
46+
assertHookHash m fpath = do
47+
actualHash <- readFileHashValue fpath
48+
hsPath <- getHooksSecurityFilePath NoFlag
49+
case Map.lookup fpath m of
50+
Nothing ->
51+
dieWithException normal $
52+
HookAcceptUnknown hsPath fpath (showHashValue actualHash)
53+
Just AcceptAlways -> pure ()
54+
Just (AcceptHash expectedHash) ->
55+
when (actualHash /= expectedHash) $
56+
dieWithException normal $
57+
HookAcceptHashMismatch
58+
hsPath
59+
fpath
60+
(showHashValue expectedHash)
61+
(showHashValue actualHash)
62+
63+
getHooksSecurityFilePath :: Flag FilePath -> IO FilePath
64+
getHooksSecurityFilePath configFileFlag = do
65+
hfpath <- getConfigFilePath configFileFlag
66+
pure $ takeDirectory hfpath </> "hooks-security"
67+
68+
loadHookHasheshMap :: Flag FilePath -> IO (Map FilePath HookAccept)
69+
loadHookHasheshMap configFileFlag = do
70+
hookFilePath <- getHooksSecurityFilePath configFileFlag
71+
handleNotExists $ fmap parseHooks (BS.readFile hookFilePath)
72+
where
73+
handleNotExists :: IO (Map FilePath HookAccept) -> IO (Map FilePath HookAccept)
74+
handleNotExists action = catchIO action $ \_ -> return mempty
75+
76+
parseHooks :: ByteString -> Map FilePath HookAccept
77+
parseHooks = Map.fromList . map parse . cleanUp . BS.lines
78+
where
79+
cleanUp :: [ByteString] -> [ByteString]
80+
cleanUp = filter (not . BS.null) . map rmComments
81+
82+
rmComments :: ByteString -> ByteString
83+
rmComments = fst . BS.breakSubstring "--"
84+
85+
parse :: ByteString -> (FilePath, HookAccept)
86+
parse bs =
87+
case BS.words bs of
88+
[fp, "AcceptAlways"] -> (BS.unpack fp, AcceptAlways)
89+
[fp, "AcceptHash"] -> buildAcceptHash fp "00"
90+
[fp, "AcceptHash", h] -> buildAcceptHash fp h
91+
_ -> error $ "Not able to parse:" ++ show bs
92+
where
93+
buildAcceptHash :: ByteString -> ByteString -> (FilePath, HookAccept)
94+
buildAcceptHash fp h =
95+
case hashValueFromHex h of
96+
Left err -> error $ "Distribution.Client.HookAccept.parse :" ++ err
97+
Right hv -> (BS.unpack fp, AcceptHash hv)

cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module Distribution.Client.ProjectBuilding.UnpackedPackage
3030
import Distribution.Client.Compat.Prelude
3131
import Prelude ()
3232

33+
import Distribution.Client.HookAccept (assertHookHash)
3334
import Distribution.Client.PackageHash (renderPackageHashInputs)
3435
import Distribution.Client.ProjectBuilding.Types
3536
import Distribution.Client.ProjectConfig
@@ -105,7 +106,7 @@ import qualified Data.ByteString.Lazy.Char8 as LBS.Char8
105106
import qualified Data.List.NonEmpty as NE
106107

107108
import Control.Exception (ErrorCall, Handler (..), SomeAsyncException, assert, catches, onException)
108-
import System.Directory (canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, removeFile)
109+
import System.Directory (canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getCurrentDirectory, removeFile)
109110
import System.FilePath (dropDrive, normalise, takeDirectory, (<.>), (</>))
110111
import System.IO (Handle, IOMode (AppendMode), withFile)
111112
import System.Semaphore (SemaphoreName (..))
@@ -697,7 +698,46 @@ buildAndInstallUnpackedPackage
697698
runConfigure
698699
PBBuildPhase{runBuild} -> do
699700
noticeProgress ProgressBuilding
701+
hooksDir <- (</> "cabalHooks") <$> getCurrentDirectory
702+
-- run preBuildHook. If it returns with 0, we assume the build was
703+
-- successful. If not, run the build.
704+
preBuildHookFile <- canonicalizePath (hooksDir </> "preBuildHook")
705+
hookExists <- doesFileExist preBuildHookFile
706+
preCode <- if hookExists then do
707+
assertHookHash (pkgConfigHookHashes pkgshared) preBuildHookFile
708+
rawSystemExitCode
709+
verbosity
710+
(Just srcdir)
711+
preBuildHookFile
712+
[ (unUnitId $ installedUnitId rpkg)
713+
, (getSymbolicPath srcdir)
714+
, (getSymbolicPath builddir)
715+
]
716+
Nothing
717+
`catchIO` (\_ -> pure (ExitFailure 10))
718+
else pure ExitSuccess
719+
-- Regardless of whether the preBuildHook exists or not, or whether it returned an
720+
-- error or not, we want to run the build command.
721+
-- If the preBuildHook downloads a cached version of the build products, the following
722+
-- should be a NOOP.
700723
runBuild
724+
-- not sure, if we want to care about a failed postBuildHook?
725+
postBuildHookFile <- canonicalizePath (hooksDir </> "postBuildHook")
726+
hookExists <- doesFileExist postBuildHookFile
727+
when hookExists $ do
728+
assertHookHash (pkgConfigHookHashes pkgshared) postBuildHookFile
729+
void $
730+
rawSystemExitCode
731+
verbosity
732+
(Just srcdir)
733+
postBuildHookFile
734+
[ (unUnitId $ installedUnitId rpkg)
735+
, (getSymbolicPath srcdir)
736+
, (getSymbolicPath builddir)
737+
, show preCode
738+
]
739+
Nothing
740+
`catchIO` (\_ -> pure (ExitFailure 10))
701741
PBHaddockPhase{runHaddock} -> do
702742
noticeProgress ProgressHaddock
703743
runHaddock

cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ convertLegacyAllPackageFlags globalFlags configFlags configExFlags installFlags
685685
} = globalFlags
686686

687687
projectConfigPackageDBs = (fmap . fmap) (interpretPackageDB Nothing) projectConfigPackageDBs_
688-
688+
projectConfigHookHashes = mempty -- :: Map FilePath HookAccept
689689
ConfigFlags
690690
{ configCommonFlags = commonFlags
691691
, configHcFlavor = projectConfigHcFlavor

cabal-install/src/Distribution/Client/ProjectConfig/Types.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import Distribution.Client.BuildReports.Types
3434
import Distribution.Client.Dependency.Types
3535
( PreSolver
3636
)
37+
import Distribution.Client.HookAccept (HookAccept (..))
3738
import Distribution.Client.Targets
3839
( UserConstraint
3940
)
@@ -228,6 +229,7 @@ data ProjectConfigShared = ProjectConfigShared
228229
, projectConfigPreferOldest :: Flag PreferOldest
229230
, projectConfigProgPathExtra :: NubList FilePath
230231
, projectConfigMultiRepl :: Flag Bool
232+
, projectConfigHookHashes :: Map FilePath HookAccept
231233
-- More things that only make sense for manual mode, not --local mode
232234
-- too much control!
233235
-- projectConfigShadowPkgs :: Flag Bool,

cabal-install/src/Distribution/Client/ProjectOrchestration.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ import qualified Data.List.NonEmpty as NE
176176
import qualified Data.Map as Map
177177
import qualified Data.Set as Set
178178
import Distribution.Client.Errors
179+
import Distribution.Client.HookAccept (loadHookHasheshMap)
180+
179181
import Distribution.Package
180182
import Distribution.Simple.Command (commandShowOptions)
181183
import Distribution.Simple.Compiler
@@ -363,13 +365,16 @@ withInstallPlan
363365
, installedPackages
364366
}
365367
action = do
368+
hookHashes <- loadHookHasheshMap (projectConfigConfigFile $ projectConfigShared projectConfig)
369+
366370
-- Take the project configuration and make a plan for how to build
367371
-- everything in the project. This is independent of any specific targets
368372
-- the user has asked for.
369373
--
370374
(elaboratedPlan, _, elaboratedShared, _, _) <-
371375
rebuildInstallPlan
372376
verbosity
377+
hookHashes
373378
distDirLayout
374379
cabalDirLayout
375380
projectConfig
@@ -392,13 +397,16 @@ runProjectPreBuildPhase
392397
, installedPackages
393398
}
394399
selectPlanSubset = do
400+
hookHashes <- loadHookHasheshMap (projectConfigConfigFile $ projectConfigShared projectConfig)
401+
395402
-- Take the project configuration and make a plan for how to build
396403
-- everything in the project. This is independent of any specific targets
397404
-- the user has asked for.
398405
--
399406
(elaboratedPlan, _, elaboratedShared, _, _) <-
400407
rebuildInstallPlan
401408
verbosity
409+
hookHashes
402410
distDirLayout
403411
cabalDirLayout
404412
projectConfig

0 commit comments

Comments
 (0)