Add extra checks for masked return codes

This commit is contained in:
Christian Nassif-Haynes 2021-09-06 05:52:34 +10:00
parent 3a296cd788
commit 093df8cb24
4 changed files with 129 additions and 2 deletions

View File

@ -260,6 +260,13 @@ optionalTreeChecks = [
cdPositive = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func && echo ok", cdPositive = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func && echo ok",
cdNegative = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func; echo ok" cdNegative = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func; echo ok"
}, checkSetESuppressed) }, checkSetESuppressed)
,(newCheckDescription {
cdName = "check-extra-masked-returns",
cdDescription = "Check for additional cases where exit codes are masked",
cdPositive = "rm -r \"$(get_chroot_dir)/home\"",
cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\""
}, checkExtraMaskedReturns)
] ]
optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment]) optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment])
@ -4734,5 +4741,107 @@ checkSetESuppressed params t =
isIn t cmds = getId t `elem` map getId cmds isIn t cmds = getId t `elem` map getId cmds
prop_checkExtraMaskedReturns1 = verifyTree checkExtraMaskedReturns "cat < <(ls)"
prop_checkExtraMaskedReturns2 = verifyTree checkExtraMaskedReturns "read -ra arr <(ls)"
prop_checkExtraMaskedReturns3 = verifyTree checkExtraMaskedReturns "ls >(cat)"
prop_checkExtraMaskedReturns4 = verifyTree checkExtraMaskedReturns "false | true"
prop_checkExtraMaskedReturns5 = verifyNotTree checkExtraMaskedReturns "set -o pipefail; false | true"
prop_checkExtraMaskedReturns6 = verifyNotTree checkExtraMaskedReturns "false | true || true"
prop_checkExtraMaskedReturns7 = verifyTree checkExtraMaskedReturns "true $(false)"
prop_checkExtraMaskedReturns8 = verifyTree checkExtraMaskedReturns "x=$(false)$(true)"
prop_checkExtraMaskedReturns9 = verifyNotTree checkExtraMaskedReturns "x=$(false)true"
prop_checkExtraMaskedReturns10 = verifyTree checkExtraMaskedReturns "x=`false``false`"
prop_checkExtraMaskedReturns11 = verifyTree checkExtraMaskedReturns "x=\"$(false)$(true)\""
prop_checkExtraMaskedReturns12 = verifyTree checkExtraMaskedReturns "x=\"$(false)\"\"$(true)\""
prop_checkExtraMaskedReturns13 = verifyTree checkExtraMaskedReturns "true <<<$(false)"
prop_checkExtraMaskedReturns14 = verifyNotTree checkExtraMaskedReturns "echo asdf | false"
prop_checkExtraMaskedReturns15 = verifyNotTree checkExtraMaskedReturns "readonly x=$(false)"
prop_checkExtraMaskedReturns16 = verifyTree checkExtraMaskedReturns "readarray -t files < <(ls)"
prop_checkExtraMaskedReturns17 = verifyNotTree checkExtraMaskedReturns "x=( $(false) false )"
prop_checkExtraMaskedReturns18 = verifyTree checkExtraMaskedReturns "x=( $(false) $(false) )"
prop_checkExtraMaskedReturns19 = verifyNotTree checkExtraMaskedReturns "x=( $(false) [4]=false )"
prop_checkExtraMaskedReturns20 = verifyTree checkExtraMaskedReturns "x=( $(false) [4]=$(false) )"
prop_checkExtraMaskedReturns21 = verifyTree checkExtraMaskedReturns "cat << foo\n $(false)\nfoo"
prop_checkExtraMaskedReturns22 = verifyTree checkExtraMaskedReturns "[[ $(false) ]]"
prop_checkExtraMaskedReturns23 = verifyNotTree checkExtraMaskedReturns "x=$(false) y=z"
prop_checkExtraMaskedReturns24 = verifyNotTree checkExtraMaskedReturns "x=$(( $(date +%s) ))"
prop_checkExtraMaskedReturns25 = verifyTree checkExtraMaskedReturns "echo $(( $(date +%s) ))"
prop_checkExtraMaskedReturns26 = verifyNotTree checkExtraMaskedReturns "x=( $(false) )"
prop_checkExtraMaskedReturns27 = verifyTree checkExtraMaskedReturns "x=$(false) false"
prop_checkExtraMaskedReturns28 = verifyTree checkExtraMaskedReturns "x=$(false) y=$(false)"
prop_checkExtraMaskedReturns29 = verifyNotTree checkExtraMaskedReturns "false < <(set -e)"
prop_checkExtraMaskedReturns30 = verifyNotTree checkExtraMaskedReturns "false < <(shopt -s cdspell)"
prop_checkExtraMaskedReturns31 = verifyNotTree checkExtraMaskedReturns "false < <(dirname \"${BASH_SOURCE[0]}\")"
prop_checkExtraMaskedReturns32 = verifyNotTree checkExtraMaskedReturns "false < <(basename \"${BASH_SOURCE[0]}\")"
prop_checkExtraMaskedReturns33 = verifyNotTree checkExtraMaskedReturns "{ false || true; } | true"
prop_checkExtraMaskedReturns34 = verifyNotTree checkExtraMaskedReturns "{ false || :; } | true"
checkExtraMaskedReturns params t = runNodeAnalysis findMaskingNodes params t
where
findMaskingNodes _ (T_Arithmetic _ list) = findMaskedNodesInList [list]
findMaskingNodes _ (T_Array _ list) = findMaskedNodesInList $ allButLastSimpleCommands list
findMaskingNodes _ (T_Condition _ _ condition) = findMaskedNodesInList [condition]
findMaskingNodes _ (T_DoubleQuoted _ list) = findMaskedNodesInList $ allButLastSimpleCommands list
findMaskingNodes _ (T_HereDoc _ _ _ _ list) = findMaskedNodesInList list
findMaskingNodes _ (T_HereString _ word) = findMaskedNodesInList [word]
findMaskingNodes _ (T_NormalWord _ parts) = findMaskedNodesInList $ allButLastSimpleCommands parts
findMaskingNodes _ (T_Pipeline _ _ cmds) | not (hasPipefail params) = findMaskedNodesInList $ allButLastSimpleCommands cmds
findMaskingNodes _ (T_ProcSub _ _ list) = findMaskedNodesInList list
findMaskingNodes _ (T_SimpleCommand _ assigns (_:args)) = findMaskedNodesInList $ assigns ++ args
findMaskingNodes _ (T_SimpleCommand _ assigns []) = findMaskedNodesInList $ allButLastSimpleCommands assigns
findMaskingNodes _ _ = return ()
findMaskedNodesInList = mapM_ (doAnalysis findMaskedNodes)
isMaskedNode t = not (isHarmlessCommand t || isCheckedElsewhere t || isMaskDeliberate t)
findMaskedNodes t@(T_SimpleCommand _ _ (_:_)) = when (isMaskedNode t) $ inform t
findMaskedNodes t@T_Condition {} = when (isMaskedNode t) $ inform t
findMaskedNodes _ = return ()
containsSimpleCommand t = isNothing $ doAnalysis go t
where
go t = case t of
T_SimpleCommand {} -> fail ""
_ -> return ()
allButLastSimpleCommands cmds =
if null simpleCommands then [] else init simpleCommands
where
simpleCommands = filter containsSimpleCommand cmds
inform t = info (getId t) 2312 ("Consider invoking this command "
++ "separately to avoid masking its return value (or use '|| true' "
++ "to ignore).")
isMaskDeliberate t = hasParent isOrIf t
where
isOrIf _ (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd]))
= getCommandBasename cmd `elem` [Just "true", Just ":"]
isOrIf _ _ = False
isCheckedElsewhere t = hasParent isDeclaringCommand t
where
isDeclaringCommand t _ = fromMaybe False $ do
basename <- getCommandBasename t
return $ basename `elem` declaringCommands
isHarmlessCommand t = fromMaybe False $ do
basename <- getCommandBasename t
return $ basename `elem` [
"echo"
,"basename"
,"dirname"
,"printf"
,"set"
,"shopt"
]
parentChildPairs t = go $ parents params t
where
go (child:parent:rest) = (parent, child):go (parent:rest)
go _ = []
hasParent pred t = any (uncurry pred) (parentChildPairs t)
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@ -83,6 +83,8 @@ data Parameters = Parameters {
hasInheritErrexit :: Bool, hasInheritErrexit :: Bool,
-- Whether this script has 'set -e' anywhere. -- Whether this script has 'set -e' anywhere.
hasSetE :: Bool, hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- A linear (bad) analysis of data flow -- A linear (bad) analysis of data flow
variableFlow :: [StackData], variableFlow :: [StackData],
-- A map from Id to parent Token -- A map from Id to parent Token
@ -204,6 +206,12 @@ makeParameters spec =
Dash -> True Dash -> True
Sh -> True Sh -> True
Ksh -> False, Ksh -> False,
hasPipefail =
case shellType params of
Bash -> containsPipefail root
Dash -> True
Sh -> True
Ksh -> containsPipefail root,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root,
@ -226,6 +234,16 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False _ -> False
re = mkRegex "[[:space:]]-[^-]*e" re = mkRegex "[[:space:]]-[^-]*e"
containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root
where
isPipefail t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("pipefail" `elem` oversimplify t ||
"o" `elem` map snd (getAllFlags t))
_ -> False
containsShopt shopt root = containsShopt shopt root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where where

View File

@ -101,8 +101,6 @@ commandChecks = [
++ map checkArgComparison declaringCommands ++ map checkArgComparison declaringCommands
++ map checkMaskedReturns declaringCommands ++ map checkMaskedReturns declaringCommands
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
optionalChecks = map fst optionalCommandChecks optionalChecks = map fst optionalCommandChecks
optionalCommandChecks :: [(CheckDescription, CommandCheck)] optionalCommandChecks :: [(CheckDescription, CommandCheck)]

View File

@ -138,3 +138,5 @@ shellForExecutable name =
_ -> Nothing _ -> Nothing
flagsForRead = "sreu:n:N:i:p:a:t:" flagsForRead = "sreu:n:N:i:p:a:t:"
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]