Skip to content

Commit a653a58

Browse files
Implement package preprocessor2
1 parent a31e047 commit a653a58

17 files changed

+752
-1
lines changed

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

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

2467
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
@@ -41,6 +41,7 @@ import OrderOfEvaluation
4141
import OutOfBounds
4242
import Pointers
4343
import Preprocessor
44+
import Preprocessor2
4445
import Representation
4546
import Scope
4647
import SideEffects1
@@ -96,6 +97,7 @@ newtype TCPPQuery =
9697
TOutOfBoundsPackageQuery(OutOfBoundsQuery q) or
9798
TPointersPackageQuery(PointersQuery q) or
9899
TPreprocessorPackageQuery(PreprocessorQuery q) or
100+
TPreprocessor2PackageQuery(Preprocessor2Query q) or
99101
TRepresentationPackageQuery(RepresentationQuery q) or
100102
TScopePackageQuery(ScopeQuery q) or
101103
TSideEffects1PackageQuery(SideEffects1Query q) or
@@ -151,6 +153,7 @@ predicate isQueryMetadata(Query query, string queryId, string ruleId, string cat
151153
isOutOfBoundsQueryMetadata(query, queryId, ruleId, category) or
152154
isPointersQueryMetadata(query, queryId, ruleId, category) or
153155
isPreprocessorQueryMetadata(query, queryId, ruleId, category) or
156+
isPreprocessor2QueryMetadata(query, queryId, ruleId, category) or
154157
isRepresentationQueryMetadata(query, queryId, ruleId, category) or
155158
isScopeQueryMetadata(query, queryId, ruleId, category) or
156159
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 error
8+
* @tags external/misra/id/rule-19-2-2
9+
* scope/single-translation-unit
10+
* maintainability
11+
* correctness
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: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 very-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+
import codeql.util.Boolean
22+
23+
/**
24+
* This regex is used to find macro arguments that appear to have critical operators in them, before
25+
* we do the expensive process of parsing them to look for parenthesis.
26+
*/
27+
pragma[noinline]
28+
string criticalOperatorRegex() {
29+
result =
30+
".*(" +
31+
concat(string op |
32+
op in [
33+
"\\*=?", "/=?", "%=?", "\\+=?", "-=?", "<<?=?", ">>?=?", "==?", "!=", "&&?=?", "\\^/?",
34+
"\\|\\|?=?", "\\?"
35+
]
36+
|
37+
op, "|"
38+
) + ").*"
39+
}
40+
41+
/**
42+
* Whether a string appears to contain a critical operator.
43+
*/
44+
bindingset[input]
45+
predicate hasCriticalOperator(string input) { input.regexpMatch(criticalOperatorRegex()) }
46+
47+
/**
48+
* A critical operator is an operator with "level" between 13 and 2, according to the MISRA C++
49+
* standard. This includes from the "multiplicative" level (13) to the "conditional" level (2).
50+
*/
51+
class CriticalOperatorExpr extends Expr {
52+
string operator;
53+
54+
CriticalOperatorExpr() {
55+
operator = this.(BinaryOperation).getOperator()
56+
or
57+
this instanceof ConditionalExpr and operator = "?"
58+
or
59+
operator = this.(Assignment).getOperator()
60+
}
61+
62+
string getOperator() { result = operator }
63+
}
64+
65+
/**
66+
* An invocation of a macro that has a parameter that is not precedence-protected with parentheses,
67+
* and that produces a critical operator expression.
68+
*
69+
* This class is used in two passes. Firstly, with `hasRiskyParameter`, to find the macro paramaters
70+
* that should be parsed for parenthesis. Secondly, with `hasNonCompliantParameter`, to parse the
71+
* risky parameters and attempt to match the produced AST to an unparenthesized occurence of that
72+
* operator in the argument text.
73+
*
74+
* For a given macro invocation to be considered risky, it must
75+
* - The macro must have a parameter that is not precedence-protected with parentheses.
76+
* - The macro must produce a critical operator expression.
77+
* - The macro must produce only expressions, statements, or variable declarations with initializers.
78+
*
79+
* For a risky macro to be non-compliant, it must hold for some values of the predicate
80+
* `hasNonCompliantParameter`.
81+
*/
82+
class RiskyMacroInvocation extends MacroInvocation {
83+
FunctionLikeMacro macro;
84+
string riskyParamName;
85+
int riskyParamIdx;
86+
87+
RiskyMacroInvocation() {
88+
macro = getMacro() and
89+
// The parameter is not precedence-protected with parentheses in the macro body.
90+
macro.parameterPrecedenceUnprotected(riskyParamIdx) and
91+
riskyParamName = macro.getParameter(riskyParamIdx) and
92+
// This macro invocation produces a critical operator expression.
93+
getAGeneratedElement() instanceof CriticalOperatorExpr and
94+
// It seems to generate an expression, statement, or variable declaration with initializer.
95+
forex(Element e | e = getAGeneratedElement() |
96+
e instanceof Expr
97+
or
98+
e instanceof Stmt
99+
or
100+
e.(Variable).getInitializer().getExpr() = getAGeneratedElement()
101+
or
102+
e.(VariableDeclarationEntry).getDeclaration().getInitializer().getExpr() =
103+
getAGeneratedElement()
104+
)
105+
}
106+
107+
/**
108+
* A stage 1 pass used to find macro parameters that are not precedence-protected, and have a
109+
* critical operator in them, and therefore need to be parsed to check for parenthesis at the
110+
* macro call-site, which is expensive.
111+
*/
112+
predicate hasRiskyParameter(string name, int index, string value) {
113+
name = riskyParamName and
114+
index = riskyParamIdx and
115+
value = getExpandedArgument(riskyParamIdx) and
116+
hasCriticalOperator(value)
117+
}
118+
119+
/**
120+
* A stage 2 pass that occurs after risky parameters have been parsed, to check for parenthesis at the macro
121+
* call-site.
122+
*
123+
* For a given macro argument to be flagged, it must:
124+
* - be risky as determined by the characteristic predicate (produce a critical operator and only
125+
* expressions, statements, etc).
126+
* - be flagged by stage 1 as a risky parameter (i.e. it must have a critical operator in it and
127+
* correspond to a macro parameter that is not precedence-protected with parentheses)
128+
* - there must be a top-level text node that contains the operator in the argument string
129+
* - the operator cannot be the first character in the string (i.e. it should not look like a
130+
* unary - or +)
131+
* - the operator cannot exist inside a generated string literal
132+
* - the operator existence of the operator should not be as a substring of "->", "++", or "--"
133+
* operators.
134+
*
135+
* The results of this predicate should be flagged by the query.
136+
*/
137+
predicate hasNonCompliantParameter(string name, int index, string value, string operator) {
138+
hasRiskyParameter(name, index, value) and
139+
exists(
140+
ParsedRoot parsedRoot, ParsedText topLevelText, string text, CriticalOperatorExpr opExpr,
141+
int opIndex
142+
|
143+
parsedRoot.getInputString() = value and
144+
(topLevelText.getParent() = parsedRoot or topLevelText = parsedRoot) and
145+
text = topLevelText.getText().trim() and
146+
opExpr = getAGeneratedElement() and
147+
operator = opExpr.getOperator() and
148+
opIndex = text.indexOf(operator) and
149+
// Ignore "->", "++", and "--" operators.
150+
not [text.substring(opIndex - 1, opIndex + 1), text.substring(opIndex, opIndex + 2)] =
151+
["--", "++", "->"] and
152+
// Ignore operators inside string literals.
153+
not exists(Literal l |
154+
l = getAGeneratedElement() and
155+
exists(l.getValue().indexOf(operator))
156+
) and
157+
// A leading operator is probably unary and not a problem.
158+
(opIndex > 0 or topLevelText.getChildIdx() > 0)
159+
)
160+
}
161+
}
162+
163+
/**
164+
* A string class that is used to determine what macro arguments will be parsed.
165+
*
166+
* This should be a reasonably small set of strings, as parsing is expensive.
167+
*/
168+
class RiskyMacroArgString extends string {
169+
RiskyMacroArgString() { any(RiskyMacroInvocation mi).hasRiskyParameter(_, _, this) }
170+
}
171+
172+
// Import `ParsedRoot` etc for the parsed macro arguments.
173+
import MatchingParenthesis<RiskyMacroArgString>
174+
175+
from
176+
RiskyMacroInvocation mi, FunctionLikeMacro m, string paramName, string criticalOperator,
177+
int paramIndex, string argumentString
178+
where
179+
not isExcluded([m.(Element), mi.(Element)],
180+
Preprocessor2Package::unparenthesizedMacroArgumentQuery()) and
181+
mi.getMacro() = m and
182+
mi.hasNonCompliantParameter(paramName, paramIndex, argumentString, criticalOperator)
183+
select mi,
184+
"Macro argument " + paramIndex + " (with expanded value '" + argumentString + "') contains a" +
185+
" critical operator '" + criticalOperator +
186+
"' that is not parenthesized, but macro $@ argument '" + paramName +
187+
"' 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 error
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)