Skip to content

Commit e6c6dfc

Browse files
gbazKleidukosmergify[bot]
authored
redownload pkgs when source hash verification fails (#8500)
* redownload pkgs when source hash verification fails * changelog * catch stray io errors reading index * cleanup TODO * Update changelog.d/pr-8500 Co-authored-by: Hécate Moonlight <[email protected]> * fix 9.4.2 build Co-authored-by: Gershom Bazerman <[email protected]> Co-authored-by: Hécate Moonlight <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 5cb8475 commit e6c6dfc

File tree

3 files changed

+56
-7
lines changed

3 files changed

+56
-7
lines changed

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
--
1212
-- Functions for fetching packages
1313
-----------------------------------------------------------------------------
14-
{-# LANGUAGE RecordWildCards #-}
14+
{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-}
1515
module Distribution.Client.FetchUtils (
1616

1717
-- * fetching packages
@@ -22,6 +22,7 @@ module Distribution.Client.FetchUtils (
2222
-- ** specifically for repo packages
2323
checkRepoTarballFetched,
2424
fetchRepoTarball,
25+
verifyFetchedTarball,
2526

2627
-- ** fetching packages asynchronously
2728
asyncFetchPackages,
@@ -43,7 +44,7 @@ import Distribution.Client.HttpUtils
4344
import Distribution.Package
4445
( PackageId, packageName, packageVersion )
4546
import Distribution.Simple.Utils
46-
( notice, info, debug, die' )
47+
( notice, info, debug, warn, die' )
4748
import Distribution.Verbosity
4849
( verboseUnmarkOutput )
4950
import Distribution.Client.GlobalFlags
@@ -56,7 +57,8 @@ import qualified Control.Exception.Safe as Safe
5657
import Control.Concurrent.Async
5758
import Control.Concurrent.MVar
5859
import System.Directory
59-
( doesFileExist, createDirectoryIfMissing, getTemporaryDirectory )
60+
( doesFileExist, createDirectoryIfMissing, getTemporaryDirectory
61+
, getFileSize )
6062
import System.IO
6163
( openTempFile, hClose )
6264
import System.FilePath
@@ -67,6 +69,8 @@ import Network.URI
6769
( URI(uriPath) )
6870

6971
import qualified Hackage.Security.Client as Sec
72+
import qualified Hackage.Security.Util.Path as Sec
73+
import qualified Hackage.Security.Util.Checked as Sec
7074

7175
-- ------------------------------------------------------------
7276
-- * Actually fetch things
@@ -118,6 +122,34 @@ checkRepoTarballFetched repo pkgid = do
118122
then return (Just file)
119123
else return Nothing
120124

125+
verifyFetchedTarball :: Verbosity -> RepoContext -> Repo -> PackageId -> IO Bool
126+
verifyFetchedTarball verbosity repoCtxt repo pkgid =
127+
let file = packageFile repo pkgid
128+
handleError :: IO Bool -> IO Bool
129+
handleError act = do
130+
res <- Safe.try act
131+
case res of
132+
Left e -> warn verbosity ("Error verifying fetched tarball " ++ file ++ ", will redownload: " ++ show (e :: SomeException)) >> pure False
133+
Right b -> pure b
134+
in handleError $ case repo of
135+
-- a secure repo has hashes we can compare against to confirm this is the correct file.
136+
RepoSecure{} ->
137+
repoContextWithSecureRepo repoCtxt repo $ \repoSecure ->
138+
Sec.withIndex repoSecure $ \callbacks ->
139+
let warnAndFail s = warn verbosity ("Fetched tarball " ++ file ++ " does not match server, will redownload: " ++ s) >> return False
140+
-- the do block in parens is due to dealing with the checked exceptions mechanism.
141+
in (do fileInfo <- Sec.indexLookupFileInfo callbacks pkgid
142+
sz <- Sec.FileLength . fromInteger <$> getFileSize file
143+
if sz /= Sec.fileInfoLength (Sec.trusted fileInfo)
144+
then warnAndFail "file length mismatch"
145+
else do
146+
res <- Sec.compareTrustedFileInfo (Sec.trusted fileInfo) <$> Sec.computeFileInfo (Sec.Path file :: Sec.Path Sec.Absolute)
147+
if res
148+
then pure True
149+
else warnAndFail "file hash mismatch")
150+
`Sec.catchChecked` (\(e :: Sec.InvalidPackageException) -> warnAndFail (show e))
151+
`Sec.catchChecked` (\(e :: Sec.VerificationError) -> warnAndFail (show e))
152+
_ -> pure True
121153

122154
-- | Fetch a package if we don't have it already.
123155
--

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ import Text.PrettyPrint (text, hang, quotes, colon, vcat, ($$), fsep,
170170
import qualified Text.PrettyPrint as Disp
171171
import qualified Data.Map as Map
172172
import qualified Data.Set as Set
173-
import Control.Monad (sequence)
173+
import Control.Monad (sequence, forM)
174174
import Control.Monad.IO.Class (liftIO)
175175
import Control.Monad.State as State (State, execState, runState, state)
176176
import Control.Exception (assert)
@@ -924,20 +924,26 @@ getPackageSourceHashes verbosity withRepoCtx solverPlan = do
924924
-- Tarballs from repositories, either where the repository provides
925925
-- hashes as part of the repo metadata, or where we will have to
926926
-- download and hash the tarball.
927-
repoTarballPkgsWithMetadata :: [(PackageId, Repo)]
927+
repoTarballPkgsWithMetadataUnvalidated :: [(PackageId, Repo)]
928928
repoTarballPkgsWithoutMetadata :: [(PackageId, Repo)]
929-
(repoTarballPkgsWithMetadata,
929+
(repoTarballPkgsWithMetadataUnvalidated,
930930
repoTarballPkgsWithoutMetadata) =
931931
partitionEithers
932932
[ case repo of
933933
RepoSecure{} -> Left (pkgid, repo)
934934
_ -> Right (pkgid, repo)
935935
| (pkgid, RepoTarballPackage repo _ _) <- allPkgLocations ]
936936

937+
(repoTarballPkgsWithMetadata, repoTarballPkgsToRedownload) <- fmap partitionEithers $
938+
liftIO $ withRepoCtx $ \repoctx -> forM repoTarballPkgsWithMetadataUnvalidated $
939+
\x@(pkg, repo) -> verifyFetchedTarball verbosity repoctx repo pkg >>= \b -> case b of
940+
True -> return $ Left x
941+
False -> return $ Right x
942+
937943
-- For tarballs from repos that do not have hashes available we now have
938944
-- to check if the packages were downloaded already.
939945
--
940-
(repoTarballPkgsToDownload,
946+
(repoTarballPkgsToDownload',
941947
repoTarballPkgsDownloaded)
942948
<- fmap partitionEithers $
943949
liftIO $ sequence
@@ -947,6 +953,7 @@ getPackageSourceHashes verbosity withRepoCtx solverPlan = do
947953
Just tarball -> return (Right (pkgid, tarball))
948954
| (pkgid, repo) <- repoTarballPkgsWithoutMetadata ]
949955

956+
let repoTarballPkgsToDownload = repoTarballPkgsToRedownload ++ repoTarballPkgsToDownload'
950957
(hashesFromRepoMetadata,
951958
repoTarballPkgsNewlyDownloaded) <-
952959
-- Avoid having to initialise the repository (ie 'withRepoCtx') if we

changelog.d/pr-8500

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
synopsis: Redownload pkgs when source hash verification fails
2+
packages: cabal-install
3+
prs: #8500
4+
issues: #7541
5+
6+
description: {
7+
8+
- Cabal-install will verify source hashes on cached downloads against the current index, and redownload on mismatch. (Which can occur with e.g. head.hackage)
9+
10+
}

0 commit comments

Comments
 (0)