Skip to content

Commit d48220c

Browse files
committed
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.
1 parent 595d023 commit d48220c

File tree

18 files changed

+299
-2
lines changed

18 files changed

+299
-2
lines changed

cabal-install/cabal-install.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ library
141141
Distribution.Client.GlobalFlags
142142
Distribution.Client.Haddock
143143
Distribution.Client.HashValue
144+
Distribution.Client.HookAccept
144145
Distribution.Client.HttpUtils
145146
Distribution.Client.IndexUtils
146147
Distribution.Client.IndexUtils.ActiveRepos

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

+1
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/CmdTarget.hs

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ targetAction flags@NixStyleFlags{..} ts globalFlags = do
160160
(_, elaboratedPlan, _, _, _) <-
161161
rebuildInstallPlan
162162
verbosity
163+
mempty
163164
distDirLayout
164165
cabalDirLayout
165166
projectConfig

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

+24
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ data CabalInstallException
186186
| MissingPackageList Repo.RemoteRepo
187187
| CmdPathAcceptsNoTargets
188188
| CmdPathCommandDoesn'tSupportDryRun
189+
| HookAcceptUnknown FilePath FilePath String
190+
| HookAcceptHashMismatch FilePath FilePath String String
189191
deriving (Show)
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
@@ -861,6 +865,26 @@ exceptionMessageCabalInstall e = case e of
861865
CmdPathCommandDoesn'tSupportDryRun ->
862866
"The 'path' command doesn't support the flag '--dry-run'."
863867

868+
HookAcceptUnknown hsPath fpath hash ->
869+
concat
870+
[ "The following file does not appear in the hooks-security file.\n"
871+
, " hook file : ", fpath, "\n"
872+
, " file hash : ", hash, "\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: ", hsPath
876+
]
877+
878+
HookAcceptHashMismatch hsPath fpath expected actual ->
879+
concat
880+
[ "\nHook file hash mismatch for:\n"
881+
, " hook file : ", fpath, "\n"
882+
, " expected hash: ", expected, "\n"
883+
, " actual hash : ", actual, "\n"
884+
, "The hook file should be inspected and if deemed ok, the hooks-security file updated.\n"
885+
, "The hooks-security file is (probably) located at: ", hsPath
886+
]
887+
864888
instance Exception (VerboseException CabalInstallException) where
865889
displayException :: VerboseException CabalInstallException -> [Char]
866890
displayException (VerboseException stack timestamp verb cabalInstallException) =

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

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module Distribution.Client.HashValue
55
( HashValue
66
, hashValue
7+
, hashValueFromHex
78
, truncateHash
89
, showHashValue
910
, readFileHashValue
@@ -51,6 +52,11 @@ instance Structured HashValue
5152
hashValue :: LBS.ByteString -> HashValue
5253
hashValue = HashValue . SHA256.hashlazy
5354

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

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

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

+37-1
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,42 @@ 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+
assertHookHash (pkgConfigHookHashes pkgshared) preBuildHookFile
706+
preCode <-
707+
rawSystemExitCode
708+
verbosity
709+
(Just srcdir)
710+
preBuildHookFile
711+
[ (unUnitId $ installedUnitId rpkg)
712+
, (getSymbolicPath srcdir)
713+
, (getSymbolicPath builddir)
714+
]
715+
Nothing
716+
`catchIO` (\_ -> pure (ExitFailure 10))
717+
-- Regardless of whether the preBuildHook exists or not, or whether it returned an
718+
-- error or not, we want to run the build command.
719+
-- If the preBuildHook downloads a cached version of the build products, the following
720+
-- should be a NOOP.
700721
runBuild
722+
-- not sure, if we want to care about a failed postBuildHook?
723+
postBuildHookFile <- canonicalizePath (hooksDir </> "postBuildHook")
724+
assertHookHash (pkgConfigHookHashes pkgshared) postBuildHookFile
725+
void $
726+
rawSystemExitCode
727+
verbosity
728+
(Just srcdir)
729+
postBuildHookFile
730+
[ (unUnitId $ installedUnitId rpkg)
731+
, (getSymbolicPath srcdir)
732+
, (getSymbolicPath builddir)
733+
, show preCode
734+
]
735+
Nothing
736+
`catchIO` (\_ -> pure (ExitFailure 10))
701737
PBHaddockPhase{runHaddock} -> do
702738
noticeProgress ProgressHaddock
703739
runHaddock

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

+2
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,8 @@ convertLegacyAllPackageFlags globalFlags configFlags configExFlags installFlags
715715

716716
projectConfigPackageDBs = (fmap . fmap) (interpretPackageDB Nothing) projectConfigPackageDBs_
717717

718+
projectConfigHookHashes = mempty -- :: Map FilePath HookAccept
719+
718720
ConfigFlags
719721
{ configCommonFlags = commonFlags
720722
, configHcFlavor = projectConfigHcFlavor

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

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import qualified Data.ByteString.Char8 as BS
3030
import Distribution.Client.BuildReports.Types
3131
( ReportLevel (..)
3232
)
33+
import Distribution.Client.HookAccept (HookAccept (..))
3334
import Distribution.Client.Dependency.Types
3435
( PreSolver
3536
)
@@ -227,6 +228,7 @@ data ProjectConfigShared = ProjectConfigShared
227228
, projectConfigPreferOldest :: Flag PreferOldest
228229
, projectConfigProgPathExtra :: NubList FilePath
229230
, projectConfigMultiRepl :: Flag Bool
231+
, projectConfigHookHashes :: Map FilePath HookAccept
230232
-- More things that only make sense for manual mode, not --local mode
231233
-- too much control!
232234
-- projectConfigShadowPkgs :: Flag Bool,

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

+8
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

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import Distribution.Client.Dependency
117117
import Distribution.Client.DistDirLayout
118118
import Distribution.Client.FetchUtils
119119
import Distribution.Client.HashValue
120+
import Distribution.Client.HookAccept (HookAccept)
120121
import Distribution.Client.HttpUtils
121122
import Distribution.Client.JobControl
122123
import Distribution.Client.PackageHash
@@ -589,6 +590,7 @@ Binary ProgramDb instance.
589590
--
590591
rebuildInstallPlan
591592
:: Verbosity
593+
-> Map FilePath HookAccept
592594
-> DistDirLayout
593595
-> CabalDirLayout
594596
-> ProjectConfig
@@ -604,6 +606,7 @@ rebuildInstallPlan
604606
-- ^ @(improvedPlan, elaboratedPlan, _, _, _)@
605607
rebuildInstallPlan
606608
verbosity
609+
hookHashes
607610
distDirLayout@DistDirLayout
608611
{ distProjectRootDirectory
609612
, distProjectCacheFile
@@ -621,7 +624,7 @@ rebuildInstallPlan
621624
fileMonitorImprovedPlan
622625
-- react to changes in the project config,
623626
-- the package .cabal files and the path
624-
(projectConfigMonitored, localPackages, progsearchpath)
627+
(projectConfigMonitored, localPackages, progsearchpath, hookHashes)
625628
$ do
626629
-- And so is the elaborated plan that the improved plan based on
627630
(elaboratedPlan, elaboratedShared, totalIndexState, activeRepos) <-
@@ -631,6 +634,7 @@ rebuildInstallPlan
631634
( projectConfigMonitored
632635
, localPackages
633636
, progsearchpath
637+
, hookHashes
634638
)
635639
$ do
636640
compilerEtc <- phaseConfigureCompiler projectConfig
@@ -737,6 +741,7 @@ rebuildInstallPlan
737741
, compiler
738742
, platform
739743
, programDbSignature progdb
744+
, hookHashes
740745
)
741746
$ do
742747
installedPkgIndex <-
@@ -865,6 +870,7 @@ rebuildInstallPlan
865870
liftIO . runLogProgress verbosity $
866871
elaborateInstallPlan
867872
verbosity
873+
hookHashes
868874
platform
869875
compiler
870876
progdb
@@ -1585,6 +1591,7 @@ planPackages
15851591
-- matching that of the classic @cabal install --user@ or @--global@
15861592
elaborateInstallPlan
15871593
:: Verbosity
1594+
-> Map FilePath HookAccept
15881595
-> Platform
15891596
-> Compiler
15901597
-> ProgramDb
@@ -1602,6 +1609,7 @@ elaborateInstallPlan
16021609
-> LogProgress (ElaboratedInstallPlan, ElaboratedSharedConfig)
16031610
elaborateInstallPlan
16041611
verbosity
1612+
hookHashes
16051613
platform
16061614
compiler
16071615
compilerprogdb
@@ -1625,6 +1633,7 @@ elaborateInstallPlan
16251633
, pkgConfigCompiler = compiler
16261634
, pkgConfigCompilerProgs = compilerprogdb
16271635
, pkgConfigReplOptions = mempty
1636+
, pkgConfigHookHashes = hookHashes
16281637
}
16291638

16301639
preexistingInstantiatedPkgs :: Map UnitId FullUnitId

0 commit comments

Comments
 (0)