Skip to content

Commit a9a7f2d

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 a9a7f2d

File tree

17 files changed

+311
-3
lines changed

17 files changed

+311
-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: 37 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,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

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

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import Distribution.Client.Dependency
108108
import Distribution.Client.DistDirLayout
109109
import Distribution.Client.FetchUtils
110110
import Distribution.Client.HashValue
111+
import Distribution.Client.HookAccept (HookAccept)
111112
import Distribution.Client.HttpUtils
112113
import Distribution.Client.JobControl
113114
import Distribution.Client.PackageHash
@@ -535,6 +536,7 @@ configureCompiler
535536
--
536537
rebuildInstallPlan
537538
:: Verbosity
539+
-> Map FilePath HookAccept
538540
-> DistDirLayout
539541
-> CabalDirLayout
540542
-> ProjectConfig
@@ -550,6 +552,7 @@ rebuildInstallPlan
550552
-- ^ @(improvedPlan, elaboratedPlan, _, _, _)@
551553
rebuildInstallPlan
552554
verbosity
555+
hookHashes
553556
distDirLayout@DistDirLayout
554557
{ distProjectRootDirectory
555558
, distProjectCacheFile
@@ -567,7 +570,7 @@ rebuildInstallPlan
567570
fileMonitorImprovedPlan
568571
-- react to changes in the project config,
569572
-- the package .cabal files and the path
570-
(projectConfigMonitored, localPackages, progsearchpath)
573+
(projectConfigMonitored, localPackages, progsearchpath, hookHashes)
571574
$ do
572575
-- And so is the elaborated plan that the improved plan based on
573576
(elaboratedPlan, elaboratedShared, totalIndexState, activeRepos) <-
@@ -577,6 +580,7 @@ rebuildInstallPlan
577580
( projectConfigMonitored
578581
, localPackages
579582
, progsearchpath
583+
, hookHashes
580584
)
581585
$ do
582586
compilerEtc <- phaseConfigureCompiler projectConfig
@@ -683,6 +687,7 @@ rebuildInstallPlan
683687
, compiler
684688
, platform
685689
, programDbSignature progdb
690+
, hookHashes
686691
)
687692
$ do
688693
installedPkgIndex <-
@@ -811,6 +816,7 @@ rebuildInstallPlan
811816
liftIO . runLogProgress verbosity $
812817
elaborateInstallPlan
813818
verbosity
819+
hookHashes
814820
platform
815821
compiler
816822
progdb
@@ -1531,6 +1537,7 @@ planPackages
15311537
-- matching that of the classic @cabal install --user@ or @--global@
15321538
elaborateInstallPlan
15331539
:: Verbosity
1540+
-> Map FilePath HookAccept
15341541
-> Platform
15351542
-> Compiler
15361543
-> ProgramDb
@@ -1548,6 +1555,7 @@ elaborateInstallPlan
15481555
-> LogProgress (ElaboratedInstallPlan, ElaboratedSharedConfig)
15491556
elaborateInstallPlan
15501557
verbosity
1558+
hookHashes
15511559
platform
15521560
compiler
15531561
compilerprogdb
@@ -1571,6 +1579,7 @@ elaborateInstallPlan
15711579
, pkgConfigCompiler = compiler
15721580
, pkgConfigCompilerProgs = compilerprogdb
15731581
, pkgConfigReplOptions = mempty
1582+
, pkgConfigHookHashes = hookHashes
15741583
}
15751584

15761585
preexistingInstantiatedPkgs :: Map UnitId FullUnitId

0 commit comments

Comments
 (0)