Skip to content

Commit 91633bf

Browse files
Merge pull request #898 from github/implement-package-preprocessor2
Implement package preprocessor2
2 parents c272090 + 5f10e7c commit 91633bf

File tree

17 files changed

+753
-1
lines changed

17 files changed

+753
-1
lines changed

cpp/common/src/codingstandards/cpp/Macro.qll

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,49 @@ class FunctionLikeMacro extends Macro {
2626
exists(this.getBody().regexpFind("\\#?\\b" + parameter + "\\b", _, result))
2727
)
2828
}
29+
30+
/**
31+
* Holds if the parameter is used in a way that may make it vulnerable to precedence issues.
32+
*
33+
* Typically, parameters are wrapped in parentheses to protect them from precedence issues, but
34+
* that is not always possible.
35+
*/
36+
predicate parameterPrecedenceUnprotected(int index) {
37+
// Check if the parameter is used in a way that requires parentheses
38+
exists(string parameter | parameter = getParameter(index) |
39+
// Finds any occurence of the parameter that is not preceded by, or followed by, either a
40+
// parenthesis or the '#' token operator.
41+
//
42+
// Note the following cases:
43+
// - "(x + 1)" is preceded by a parenthesis, but not followed by one, so SHOULD be matched.
44+
// - "x # 1" is followed by "#" (though not preceded by #) and SHOULD be matched.
45+
// - "(1 + x)" is followed by a parenthesis, but not preceded by one, so SHOULD be matched.
46+
// - "1 # x" is preceded by "#" (though not followed by #) and SHOULD NOT be matched.
47+
//
48+
// So the regex is structured as follows:
49+
// - paramMatch: Matches the parameter at a word boundary, with optional whitespace
50+
// - notHashed: Finds parameters not used with a leading # operator.
51+
// - The final regex finds cases of `notHashed` that are not preceded by a parenthesis,
52+
// and cases of `notHashed` that are not followed by a parenthesis.
53+
//
54+
// Therefore, a parameter with parenthesis on both sides is not matched, a parameter with
55+
// parenthesis missing on one or both sides is only matched if there is no leading or trailing
56+
// ## operator.
57+
exists(string noBeforeParen, string noAfterParen, string paramMatch, string notHashed |
58+
// Not preceded by a parenthesis
59+
noBeforeParen = "(?<!\\(\\s*)" and
60+
// Not followed by a parenthesis
61+
noAfterParen = "(?!\\s*\\))" and
62+
// Parameter at word boundary in optional whitespace
63+
paramMatch = "\\s*\\b" + parameter + "\\b\\s*" and
64+
// A parameter is #'d if it is preceded or followed by the # or ## operators.
65+
notHashed = "(?<!#)" + paramMatch and
66+
// Parameter is used without a leading or trailing parenthesis, and without #.
67+
getBody()
68+
.regexpMatch(".*(" + noBeforeParen + notHashed + "|" + notHashed + noAfterParen + ").*")
69+
)
70+
)
71+
}
2972
}
3073

3174
newtype TMacroOperator =

cpp/common/src/codingstandards/cpp/MatchingParenthesis.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ module MatchingParenthesis<InputString Input> {
6161
occurrence = prevOccurrence + 1
6262
) else (
6363
token = TNotParen() and
64-
exists(inputStr.regexpFind("\\(|\\)", prevOccurrence + 1, endPos)) and
64+
exists(inputStr.regexpFind("\\(|\\)|$", prevOccurrence + 1, endPos)) and
6565
occurrence = prevOccurrence
6666
)
6767
)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//** THIS FILE IS AUTOGENERATED, DO NOT MODIFY DIRECTLY. **/
2+
import cpp
3+
import RuleMetadata
4+
import codingstandards.cpp.exclusions.RuleMetadata
5+
6+
newtype Preprocessor2Query =
7+
TInvalidIncludeDirectiveQuery() or
8+
TUnparenthesizedMacroArgumentQuery() or
9+
TDisallowedUseOfPragmaQuery()
10+
11+
predicate isPreprocessor2QueryMetadata(Query query, string queryId, string ruleId, string category) {
12+
query =
13+
// `Query` instance for the `invalidIncludeDirective` query
14+
Preprocessor2Package::invalidIncludeDirectiveQuery() and
15+
queryId =
16+
// `@id` for the `invalidIncludeDirective` query
17+
"cpp/misra/invalid-include-directive" and
18+
ruleId = "RULE-19-2-2" and
19+
category = "required"
20+
or
21+
query =
22+
// `Query` instance for the `unparenthesizedMacroArgument` query
23+
Preprocessor2Package::unparenthesizedMacroArgumentQuery() and
24+
queryId =
25+
// `@id` for the `unparenthesizedMacroArgument` query
26+
"cpp/misra/unparenthesized-macro-argument" and
27+
ruleId = "RULE-19-3-4" and
28+
category = "required"
29+
or
30+
query =
31+
// `Query` instance for the `disallowedUseOfPragma` query
32+
Preprocessor2Package::disallowedUseOfPragmaQuery() and
33+
queryId =
34+
// `@id` for the `disallowedUseOfPragma` query
35+
"cpp/misra/disallowed-use-of-pragma" and
36+
ruleId = "RULE-19-6-1" and
37+
category = "advisory"
38+
}
39+
40+
module Preprocessor2Package {
41+
Query invalidIncludeDirectiveQuery() {
42+
//autogenerate `Query` type
43+
result =
44+
// `Query` type for `invalidIncludeDirective` query
45+
TQueryCPP(TPreprocessor2PackageQuery(TInvalidIncludeDirectiveQuery()))
46+
}
47+
48+
Query unparenthesizedMacroArgumentQuery() {
49+
//autogenerate `Query` type
50+
result =
51+
// `Query` type for `unparenthesizedMacroArgument` query
52+
TQueryCPP(TPreprocessor2PackageQuery(TUnparenthesizedMacroArgumentQuery()))
53+
}
54+
55+
Query disallowedUseOfPragmaQuery() {
56+
//autogenerate `Query` type
57+
result =
58+
// `Query` type for `disallowedUseOfPragma` query
59+
TQueryCPP(TPreprocessor2PackageQuery(TDisallowedUseOfPragmaQuery()))
60+
}
61+
}

cpp/common/src/codingstandards/cpp/exclusions/cpp/RuleMetadata.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import Pointers
6767
import Preconditions1
6868
import Preconditions4
6969
import Preprocessor
70+
import Preprocessor2
7071
import Representation
7172
import Scope
7273
import SideEffects1
@@ -153,6 +154,7 @@ newtype TCPPQuery =
153154
TPreconditions1PackageQuery(Preconditions1Query q) or
154155
TPreconditions4PackageQuery(Preconditions4Query q) or
155156
TPreprocessorPackageQuery(PreprocessorQuery q) or
157+
TPreprocessor2PackageQuery(Preprocessor2Query q) or
156158
TRepresentationPackageQuery(RepresentationQuery q) or
157159
TScopePackageQuery(ScopeQuery q) or
158160
TSideEffects1PackageQuery(SideEffects1Query q) or
@@ -239,6 +241,7 @@ predicate isQueryMetadata(Query query, string queryId, string ruleId, string cat
239241
isPreconditions1QueryMetadata(query, queryId, ruleId, category) or
240242
isPreconditions4QueryMetadata(query, queryId, ruleId, category) or
241243
isPreprocessorQueryMetadata(query, queryId, ruleId, category) or
244+
isPreprocessor2QueryMetadata(query, queryId, ruleId, category) or
242245
isRepresentationQueryMetadata(query, queryId, ruleId, category) or
243246
isScopeQueryMetadata(query, queryId, ruleId, category) or
244247
isSideEffects1QueryMetadata(query, queryId, ruleId, category) or
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @id cpp/misra/invalid-include-directive
3+
* @name RULE-19-2-2: The #include directive shall be followed by either a <filename> or "filename" sequence
4+
* @description Include directives shall only use the <filename> or "filename" forms.
5+
* @kind problem
6+
* @precision very-high
7+
* @problem.severity warning
8+
* @tags external/misra/id/rule-19-2-2
9+
* scope/single-translation-unit
10+
* maintainability
11+
* readability
12+
* external/misra/enforcement/decidable
13+
* external/misra/obligation/required
14+
*/
15+
16+
import cpp
17+
import codingstandards.cpp.misra
18+
19+
from Include include
20+
where
21+
not isExcluded(include, Preprocessor2Package::invalidIncludeDirectiveQuery()) and
22+
// Check for < followed by (not >)+ followed by >, or " followed by (not ")+ followed by ".
23+
not include.getIncludeText().trim().regexpMatch("^(<[^>]+>|\"[^\"]+\")$")
24+
select include, "Non-compliant #include directive text '" + include.getHead() + "'."
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* @id cpp/misra/unparenthesized-macro-argument
3+
* @name RULE-19-3-4: Parentheses shall be used to ensure macro arguments are expanded appropriately
4+
* @description Expanded macro arguments shall be enclosed in parentheses to ensure the resulting
5+
* expressions have the expected precedence and order of operations.
6+
* @kind problem
7+
* @precision high
8+
* @problem.severity error
9+
* @tags external/misra/id/rule-19-3-4
10+
* scope/single-translation-unit
11+
* correctness
12+
* maintainability
13+
* external/misra/enforcement/decidable
14+
* external/misra/obligation/required
15+
*/
16+
17+
import cpp
18+
import codingstandards.cpp.misra
19+
import codingstandards.cpp.Macro
20+
import codingstandards.cpp.MatchingParenthesis
21+
22+
/**
23+
* This regex is used to find macro arguments that appear to have critical operators in them, before
24+
* we do the expensive process of parsing them to look for parenthesis.
25+
*/
26+
pragma[noinline]
27+
string criticalOperatorRegex() {
28+
result =
29+
".*(" +
30+
concat(string op |
31+
op in [
32+
"\\*=?", "/=?", "%=?", "\\+=?", "-=?", "<<?=?", ">>?=?", "==?", "!=", "&&?=?", "\\^/?",
33+
"\\|\\|?=?", "\\?"
34+
]
35+
|
36+
op, "|"
37+
) + ").*"
38+
}
39+
40+
/**
41+
* Whether a string appears to contain a critical operator.
42+
*/
43+
bindingset[input]
44+
predicate hasCriticalOperator(string input) { input.regexpMatch(criticalOperatorRegex()) }
45+
46+
/**
47+
* A critical operator is an operator with "level" between 13 and 2, according to the MISRA C++
48+
* standard. This includes from the "multiplicative" level (13) to the "conditional" level (2).
49+
*/
50+
class CriticalOperatorExpr extends Expr {
51+
string operator;
52+
53+
CriticalOperatorExpr() {
54+
operator = this.(BinaryOperation).getOperator()
55+
or
56+
this instanceof ConditionalExpr and operator = "?"
57+
or
58+
operator = this.(Assignment).getOperator()
59+
}
60+
61+
string getOperator() { result = operator }
62+
}
63+
64+
/**
65+
* An invocation of a macro that has a parameter that is not precedence-protected with parentheses,
66+
* and that produces a critical operator expression.
67+
*
68+
* This class is used in two passes. Firstly, with `hasRiskyParameter`, to find the macro parameters
69+
* that should be parsed for parenthesis. Secondly, with `hasNonCompliantParameter`, to parse the
70+
* risky parameters and attempt to match the produced AST to an unparenthesized occurrence of that
71+
* operator in the argument text.
72+
*
73+
* For a given macro invocation to be considered risky, it must
74+
* - The macro must have a parameter that is not precedence-protected with parentheses.
75+
* - The macro must produce a critical operator expression.
76+
* - The macro must produce only expressions, statements, or variable declarations with initializers.
77+
*
78+
* For a risky macro to be non-compliant, it must hold for some values of the predicate
79+
* `hasNonCompliantParameter`.
80+
*/
81+
class RiskyMacroInvocation extends MacroInvocation {
82+
FunctionLikeMacro macro;
83+
string riskyParamName;
84+
int riskyParamIdx;
85+
86+
RiskyMacroInvocation() {
87+
macro = getMacro() and
88+
// The parameter is not precedence-protected with parentheses in the macro body.
89+
macro.parameterPrecedenceUnprotected(riskyParamIdx) and
90+
riskyParamName = macro.getParameter(riskyParamIdx) and
91+
// This macro invocation produces a critical operator expression.
92+
getAGeneratedElement() instanceof CriticalOperatorExpr and
93+
// It seems to generate an expression, statement, or variable declaration with initializer.
94+
forex(Element e | e = getAGeneratedElement() |
95+
e instanceof Expr
96+
or
97+
e instanceof Stmt
98+
or
99+
e.(Variable).getInitializer().getExpr() = getAGeneratedElement()
100+
or
101+
e.(VariableDeclarationEntry).getDeclaration().getInitializer().getExpr() =
102+
getAGeneratedElement()
103+
)
104+
}
105+
106+
/**
107+
* A stage 1 pass used to find macro parameters that are not precedence-protected, and have a
108+
* critical operator in them, and therefore need to be parsed to check for parenthesis at the
109+
* macro call-site, which is expensive.
110+
*/
111+
predicate hasRiskyParameter(string name, int index, string value) {
112+
name = riskyParamName and
113+
index = riskyParamIdx and
114+
value = getExpandedArgument(riskyParamIdx) and
115+
hasCriticalOperator(value)
116+
}
117+
118+
/**
119+
* A stage 2 pass that occurs after risky parameters have been parsed, to check for parenthesis at the macro
120+
* call-site.
121+
*
122+
* For a given macro argument to be flagged, it must:
123+
* - be risky as determined by the characteristic predicate (produce a critical operator and only
124+
* expressions, statements, etc).
125+
* - be flagged by stage 1 as a risky parameter (i.e. it must have a critical operator in it and
126+
* correspond to a macro parameter that is not precedence-protected with parentheses)
127+
* - there must be a top-level text node that contains the operator in the argument string
128+
* - the operator cannot be the first character in the string (i.e. it should not look like a
129+
* unary - or +)
130+
* - the operator cannot exist inside a generated string literal
131+
* - the operator should not be found inside a "->", "++", or "--" operator.
132+
*
133+
* The results of this predicate should be flagged by the query.
134+
*/
135+
predicate hasNonCompliantParameter(string name, int index, string value, string operator) {
136+
hasRiskyParameter(name, index, value) and
137+
exists(
138+
ParsedRoot parsedRoot, ParsedText topLevelText, string text, CriticalOperatorExpr opExpr,
139+
int opIndex
140+
|
141+
parsedRoot.getInputString() = value and
142+
parsedRoot = topLevelText.getParent() and
143+
text = topLevelText.getText().trim() and
144+
opExpr = getAGeneratedElement() and
145+
operator = opExpr.getOperator() and
146+
opIndex = text.indexOf(operator) and
147+
// Ignore "->", "++", and "--" operators.
148+
not [text.substring(opIndex - 1, opIndex + 1), text.substring(opIndex, opIndex + 2)] =
149+
["--", "++", "->"] and
150+
// Ignore operators inside string literals.
151+
not exists(Literal l |
152+
l = getAGeneratedElement() and
153+
exists(l.getValue().indexOf(operator))
154+
) and
155+
// A leading operator is probably unary and not a problem.
156+
(opIndex > 0 or topLevelText.getChildIdx() > 0)
157+
)
158+
}
159+
}
160+
161+
/**
162+
* A string class that is used to determine what macro arguments will be parsed.
163+
*
164+
* This should be a reasonably small set of strings, as parsing is expensive.
165+
*/
166+
class RiskyMacroArgString extends string {
167+
RiskyMacroArgString() { any(RiskyMacroInvocation mi).hasRiskyParameter(_, _, this) }
168+
}
169+
170+
// Import `ParsedRoot` etc for the parsed macro arguments.
171+
import MatchingParenthesis<RiskyMacroArgString>
172+
173+
from
174+
RiskyMacroInvocation mi, FunctionLikeMacro m, string paramName, string criticalOperator,
175+
int paramIndex, string argumentString
176+
where
177+
not isExcluded([m.(Element), mi.(Element)],
178+
Preprocessor2Package::unparenthesizedMacroArgumentQuery()) and
179+
mi.getMacro() = m and
180+
mi.hasNonCompliantParameter(paramName, paramIndex, argumentString, criticalOperator)
181+
select mi,
182+
"Macro argument " + paramIndex + " (with expanded value '" + argumentString + "') contains a" +
183+
" critical operator '" + criticalOperator +
184+
"' that is not parenthesized, but macro $@ argument '" + paramName +
185+
"' is not precedence-protected with parenthesis.", m, m.getName()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @id cpp/misra/disallowed-use-of-pragma
3+
* @name RULE-19-6-1: The #pragma directive and the _Pragma operator should not be used
4+
* @description Preprocessor pragma directives are implementation-defined, and should not be used to
5+
* maintain code portability.
6+
* @kind problem
7+
* @precision very-high
8+
* @problem.severity warning
9+
* @tags external/misra/id/rule-19-6-1
10+
* scope/single-translation-unit
11+
* maintainability
12+
* external/misra/enforcement/decidable
13+
* external/misra/obligation/advisory
14+
*/
15+
16+
import cpp
17+
import codingstandards.cpp.misra
18+
19+
from PreprocessorDirective pragma, string kind
20+
where
21+
not isExcluded(pragma, Preprocessor2Package::disallowedUseOfPragmaQuery()) and
22+
(
23+
pragma instanceof PreprocessorPragma and
24+
kind = "#pragma directive '" + pragma.getHead() + "'"
25+
or
26+
exists(string headOrBody, string pragmaOperand |
27+
headOrBody = [pragma.getHead(), pragma.(Macro).getBody()] and
28+
pragmaOperand = headOrBody.regexpCapture(".*\\b(_Pragma\\b\\s*\\([^\\)]+\\)).*", 1) and
29+
kind = "_Pragma operator used: '" + pragmaOperand + "'"
30+
)
31+
)
32+
select pragma, "Non-compliant " + kind + "."
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| test.cpp:6:1:6:20 | #include STRING_PATH | Non-compliant #include directive text 'STRING_PATH'. |
2+
| test.cpp:10:1:10:16 | #include QSTRING | Non-compliant #include directive text 'QSTRING'. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rules/RULE-19-2-2/InvalidIncludeDirective.ql

0 commit comments

Comments
 (0)