diff --git a/cpp/common/src/codingstandards/cpp/Macro.qll b/cpp/common/src/codingstandards/cpp/Macro.qll index 3b420ae84..4b71331ed 100644 --- a/cpp/common/src/codingstandards/cpp/Macro.qll +++ b/cpp/common/src/codingstandards/cpp/Macro.qll @@ -26,6 +26,49 @@ class FunctionLikeMacro extends Macro { exists(this.getBody().regexpFind("\\#?\\b" + parameter + "\\b", _, result)) ) } + + /** + * Holds if the parameter is used in a way that may make it vulnerable to precedence issues. + * + * Typically, parameters are wrapped in parentheses to protect them from precedence issues, but + * that is not always possible. + */ + predicate parameterPrecedenceUnprotected(int index) { + // Check if the parameter is used in a way that requires parentheses + exists(string parameter | parameter = getParameter(index) | + // Finds any occurence of the parameter that is not preceded by, or followed by, either a + // parenthesis or the '#' token operator. + // + // Note the following cases: + // - "(x + 1)" is preceded by a parenthesis, but not followed by one, so SHOULD be matched. + // - "x # 1" is followed by "#" (though not preceded by #) and SHOULD be matched. + // - "(1 + x)" is followed by a parenthesis, but not preceded by one, so SHOULD be matched. + // - "1 # x" is preceded by "#" (though not followed by #) and SHOULD NOT be matched. + // + // So the regex is structured as follows: + // - paramMatch: Matches the parameter at a word boundary, with optional whitespace + // - notHashed: Finds parameters not used with a leading # operator. + // - The final regex finds cases of `notHashed` that are not preceded by a parenthesis, + // and cases of `notHashed` that are not followed by a parenthesis. + // + // Therefore, a parameter with parenthesis on both sides is not matched, a parameter with + // parenthesis missing on one or both sides is only matched if there is no leading or trailing + // ## operator. + exists(string noBeforeParen, string noAfterParen, string paramMatch, string notHashed | + // Not preceded by a parenthesis + noBeforeParen = "(? { occurrence = prevOccurrence + 1 ) else ( token = TNotParen() and - exists(inputStr.regexpFind("\\(|\\)", prevOccurrence + 1, endPos)) and + exists(inputStr.regexpFind("\\(|\\)|$", prevOccurrence + 1, endPos)) and occurrence = prevOccurrence ) ) diff --git a/cpp/common/src/codingstandards/cpp/exclusions/cpp/Preprocessor2.qll b/cpp/common/src/codingstandards/cpp/exclusions/cpp/Preprocessor2.qll new file mode 100644 index 000000000..0993b099f --- /dev/null +++ b/cpp/common/src/codingstandards/cpp/exclusions/cpp/Preprocessor2.qll @@ -0,0 +1,61 @@ +//** THIS FILE IS AUTOGENERATED, DO NOT MODIFY DIRECTLY. **/ +import cpp +import RuleMetadata +import codingstandards.cpp.exclusions.RuleMetadata + +newtype Preprocessor2Query = + TInvalidIncludeDirectiveQuery() or + TUnparenthesizedMacroArgumentQuery() or + TDisallowedUseOfPragmaQuery() + +predicate isPreprocessor2QueryMetadata(Query query, string queryId, string ruleId, string category) { + query = + // `Query` instance for the `invalidIncludeDirective` query + Preprocessor2Package::invalidIncludeDirectiveQuery() and + queryId = + // `@id` for the `invalidIncludeDirective` query + "cpp/misra/invalid-include-directive" and + ruleId = "RULE-19-2-2" and + category = "required" + or + query = + // `Query` instance for the `unparenthesizedMacroArgument` query + Preprocessor2Package::unparenthesizedMacroArgumentQuery() and + queryId = + // `@id` for the `unparenthesizedMacroArgument` query + "cpp/misra/unparenthesized-macro-argument" and + ruleId = "RULE-19-3-4" and + category = "required" + or + query = + // `Query` instance for the `disallowedUseOfPragma` query + Preprocessor2Package::disallowedUseOfPragmaQuery() and + queryId = + // `@id` for the `disallowedUseOfPragma` query + "cpp/misra/disallowed-use-of-pragma" and + ruleId = "RULE-19-6-1" and + category = "advisory" +} + +module Preprocessor2Package { + Query invalidIncludeDirectiveQuery() { + //autogenerate `Query` type + result = + // `Query` type for `invalidIncludeDirective` query + TQueryCPP(TPreprocessor2PackageQuery(TInvalidIncludeDirectiveQuery())) + } + + Query unparenthesizedMacroArgumentQuery() { + //autogenerate `Query` type + result = + // `Query` type for `unparenthesizedMacroArgument` query + TQueryCPP(TPreprocessor2PackageQuery(TUnparenthesizedMacroArgumentQuery())) + } + + Query disallowedUseOfPragmaQuery() { + //autogenerate `Query` type + result = + // `Query` type for `disallowedUseOfPragma` query + TQueryCPP(TPreprocessor2PackageQuery(TDisallowedUseOfPragmaQuery())) + } +} diff --git a/cpp/common/src/codingstandards/cpp/exclusions/cpp/RuleMetadata.qll b/cpp/common/src/codingstandards/cpp/exclusions/cpp/RuleMetadata.qll index 4bbcf6d0e..1aecbbc62 100644 --- a/cpp/common/src/codingstandards/cpp/exclusions/cpp/RuleMetadata.qll +++ b/cpp/common/src/codingstandards/cpp/exclusions/cpp/RuleMetadata.qll @@ -67,6 +67,7 @@ import Pointers import Preconditions1 import Preconditions4 import Preprocessor +import Preprocessor2 import Representation import Scope import SideEffects1 @@ -151,6 +152,7 @@ newtype TCPPQuery = TPreconditions1PackageQuery(Preconditions1Query q) or TPreconditions4PackageQuery(Preconditions4Query q) or TPreprocessorPackageQuery(PreprocessorQuery q) or + TPreprocessor2PackageQuery(Preprocessor2Query q) or TRepresentationPackageQuery(RepresentationQuery q) or TScopePackageQuery(ScopeQuery q) or TSideEffects1PackageQuery(SideEffects1Query q) or @@ -235,6 +237,7 @@ predicate isQueryMetadata(Query query, string queryId, string ruleId, string cat isPreconditions1QueryMetadata(query, queryId, ruleId, category) or isPreconditions4QueryMetadata(query, queryId, ruleId, category) or isPreprocessorQueryMetadata(query, queryId, ruleId, category) or + isPreprocessor2QueryMetadata(query, queryId, ruleId, category) or isRepresentationQueryMetadata(query, queryId, ruleId, category) or isScopeQueryMetadata(query, queryId, ruleId, category) or isSideEffects1QueryMetadata(query, queryId, ruleId, category) or diff --git a/cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql b/cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql new file mode 100644 index 000000000..2e49b34d4 --- /dev/null +++ b/cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql @@ -0,0 +1,24 @@ +/** + * @id cpp/misra/invalid-include-directive + * @name RULE-19-2-2: The #include directive shall be followed by either a or "filename" sequence + * @description Include directives shall only use the or "filename" forms. + * @kind problem + * @precision very-high + * @problem.severity warning + * @tags external/misra/id/rule-19-2-2 + * scope/single-translation-unit + * maintainability + * readability + * external/misra/enforcement/decidable + * external/misra/obligation/required + */ + +import cpp +import codingstandards.cpp.misra + +from Include include +where + not isExcluded(include, Preprocessor2Package::invalidIncludeDirectiveQuery()) and + // Check for < followed by (not >)+ followed by >, or " followed by (not ")+ followed by ". + not include.getIncludeText().trim().regexpMatch("^(<[^>]+>|\"[^\"]+\")$") +select include, "Non-compliant #include directive text '" + include.getHead() + "'." diff --git a/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql new file mode 100644 index 000000000..09cada7b0 --- /dev/null +++ b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql @@ -0,0 +1,187 @@ +/** + * @id cpp/misra/unparenthesized-macro-argument + * @name RULE-19-3-4: Parentheses shall be used to ensure macro arguments are expanded appropriately + * @description Expanded macro arguments shall be enclosed in parentheses to ensure the resulting + * expressions have the expected precedence and order of operations. + * @kind problem + * @precision high + * @problem.severity error + * @tags external/misra/id/rule-19-3-4 + * scope/single-translation-unit + * correctness + * maintainability + * external/misra/enforcement/decidable + * external/misra/obligation/required + */ + +import cpp +import codingstandards.cpp.misra +import codingstandards.cpp.Macro +import codingstandards.cpp.MatchingParenthesis +import codeql.util.Boolean + +/** + * This regex is used to find macro arguments that appear to have critical operators in them, before + * we do the expensive process of parsing them to look for parenthesis. + */ +pragma[noinline] +string criticalOperatorRegex() { + result = + ".*(" + + concat(string op | + op in [ + "\\*=?", "/=?", "%=?", "\\+=?", "-=?", "<>?=?", "==?", "!=", "&&?=?", "\\^/?", + "\\|\\|?=?", "\\?" + ] + | + op, "|" + ) + ").*" +} + +/** + * Whether a string appears to contain a critical operator. + */ +bindingset[input] +predicate hasCriticalOperator(string input) { input.regexpMatch(criticalOperatorRegex()) } + +/** + * A critical operator is an operator with "level" between 13 and 2, according to the MISRA C++ + * standard. This includes from the "multiplicative" level (13) to the "conditional" level (2). + */ +class CriticalOperatorExpr extends Expr { + string operator; + + CriticalOperatorExpr() { + operator = this.(BinaryOperation).getOperator() + or + this instanceof ConditionalExpr and operator = "?" + or + operator = this.(Assignment).getOperator() + } + + string getOperator() { result = operator } +} + +/** + * An invocation of a macro that has a parameter that is not precedence-protected with parentheses, + * and that produces a critical operator expression. + * + * This class is used in two passes. Firstly, with `hasRiskyParameter`, to find the macro paramaters + * that should be parsed for parenthesis. Secondly, with `hasNonCompliantParameter`, to parse the + * risky parameters and attempt to match the produced AST to an unparenthesized occurence of that + * operator in the argument text. + * + * For a given macro invocation to be considered risky, it must + * - The macro must have a parameter that is not precedence-protected with parentheses. + * - The macro must produce a critical operator expression. + * - The macro must produce only expressions, statements, or variable declarations with initializers. + * + * For a risky macro to be non-compliant, it must hold for some values of the predicate + * `hasNonCompliantParameter`. + */ +class RiskyMacroInvocation extends MacroInvocation { + FunctionLikeMacro macro; + string riskyParamName; + int riskyParamIdx; + + RiskyMacroInvocation() { + macro = getMacro() and + // The parameter is not precedence-protected with parentheses in the macro body. + macro.parameterPrecedenceUnprotected(riskyParamIdx) and + riskyParamName = macro.getParameter(riskyParamIdx) and + // This macro invocation produces a critical operator expression. + getAGeneratedElement() instanceof CriticalOperatorExpr and + // It seems to generate an expression, statement, or variable declaration with initializer. + forex(Element e | e = getAGeneratedElement() | + e instanceof Expr + or + e instanceof Stmt + or + e.(Variable).getInitializer().getExpr() = getAGeneratedElement() + or + e.(VariableDeclarationEntry).getDeclaration().getInitializer().getExpr() = + getAGeneratedElement() + ) + } + + /** + * A stage 1 pass used to find macro parameters that are not precedence-protected, and have a + * critical operator in them, and therefore need to be parsed to check for parenthesis at the + * macro call-site, which is expensive. + */ + predicate hasRiskyParameter(string name, int index, string value) { + name = riskyParamName and + index = riskyParamIdx and + value = getExpandedArgument(riskyParamIdx) and + hasCriticalOperator(value) + } + + /** + * A stage 2 pass that occurs after risky parameters have been parsed, to check for parenthesis at the macro + * call-site. + * + * For a given macro argument to be flagged, it must: + * - be risky as determined by the characteristic predicate (produce a critical operator and only + * expressions, statements, etc). + * - be flagged by stage 1 as a risky parameter (i.e. it must have a critical operator in it and + * correspond to a macro parameter that is not precedence-protected with parentheses) + * - there must be a top-level text node that contains the operator in the argument string + * - the operator cannot be the first character in the string (i.e. it should not look like a + * unary - or +) + * - the operator cannot exist inside a generated string literal + * - the operator existence of the operator should not be as a substring of "->", "++", or "--" + * operators. + * + * The results of this predicate should be flagged by the query. + */ + predicate hasNonCompliantParameter(string name, int index, string value, string operator) { + hasRiskyParameter(name, index, value) and + exists( + ParsedRoot parsedRoot, ParsedText topLevelText, string text, CriticalOperatorExpr opExpr, + int opIndex + | + parsedRoot.getInputString() = value and + (topLevelText.getParent() = parsedRoot or topLevelText = parsedRoot) and + text = topLevelText.getText().trim() and + opExpr = getAGeneratedElement() and + operator = opExpr.getOperator() and + opIndex = text.indexOf(operator) and + // Ignore "->", "++", and "--" operators. + not [text.substring(opIndex - 1, opIndex + 1), text.substring(opIndex, opIndex + 2)] = + ["--", "++", "->"] and + // Ignore operators inside string literals. + not exists(Literal l | + l = getAGeneratedElement() and + exists(l.getValue().indexOf(operator)) + ) and + // A leading operator is probably unary and not a problem. + (opIndex > 0 or topLevelText.getChildIdx() > 0) + ) + } +} + +/** + * A string class that is used to determine what macro arguments will be parsed. + * + * This should be a reasonably small set of strings, as parsing is expensive. + */ +class RiskyMacroArgString extends string { + RiskyMacroArgString() { any(RiskyMacroInvocation mi).hasRiskyParameter(_, _, this) } +} + +// Import `ParsedRoot` etc for the parsed macro arguments. +import MatchingParenthesis + +from + RiskyMacroInvocation mi, FunctionLikeMacro m, string paramName, string criticalOperator, + int paramIndex, string argumentString +where + not isExcluded([m.(Element), mi.(Element)], + Preprocessor2Package::unparenthesizedMacroArgumentQuery()) and + mi.getMacro() = m and + mi.hasNonCompliantParameter(paramName, paramIndex, argumentString, criticalOperator) +select mi, + "Macro argument " + paramIndex + " (with expanded value '" + argumentString + "') contains a" + + " critical operator '" + criticalOperator + + "' that is not parenthesized, but macro $@ argument '" + paramName + + "' is not precedence-protected with parenthesis.", m, m.getName() diff --git a/cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql b/cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql new file mode 100644 index 000000000..0e07ee02f --- /dev/null +++ b/cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql @@ -0,0 +1,32 @@ +/** + * @id cpp/misra/disallowed-use-of-pragma + * @name RULE-19-6-1: The #pragma directive and the _Pragma operator should not be used + * @description Preprocessor pragma directives are implementation-defined, and should not be used to + * maintain code portability. + * @kind problem + * @precision very-high + * @problem.severity warning + * @tags external/misra/id/rule-19-6-1 + * scope/single-translation-unit + * maintainability + * external/misra/enforcement/decidable + * external/misra/obligation/advisory + */ + +import cpp +import codingstandards.cpp.misra + +from PreprocessorDirective pragma, string kind +where + not isExcluded(pragma, Preprocessor2Package::disallowedUseOfPragmaQuery()) and + ( + pragma instanceof PreprocessorPragma and + kind = "#pragma directive '" + pragma.getHead() + "'" + or + exists(string headOrBody, string pragmaOperand | + headOrBody = [pragma.getHead(), pragma.(Macro).getBody()] and + pragmaOperand = headOrBody.regexpCapture(".*\\b(_Pragma\\b\\s*\\([^\\)]+\\)).*", 1) and + kind = "_Pragma operator used: '" + pragmaOperand + "'" + ) + ) +select pragma, "Non-compliant " + kind + "." diff --git a/cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.expected b/cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.expected new file mode 100644 index 000000000..4e85513b0 --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.expected @@ -0,0 +1,2 @@ +| test.cpp:6:1:6:20 | #include STRING_PATH | Non-compliant #include directive text 'STRING_PATH'. | +| test.cpp:10:1:10:16 | #include QSTRING | Non-compliant #include directive text 'QSTRING'. | diff --git a/cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.qlref b/cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.qlref new file mode 100644 index 000000000..6268670c9 --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.qlref @@ -0,0 +1 @@ +rules/RULE-19-2-2/InvalidIncludeDirective.ql \ No newline at end of file diff --git a/cpp/misra/test/rules/RULE-19-2-2/test.cpp b/cpp/misra/test/rules/RULE-19-2-2/test.cpp new file mode 100644 index 000000000..b1788458e --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-2-2/test.cpp @@ -0,0 +1,15 @@ +#define QSTRING "string" +#define DOTH .h +#define STRING_PATH "string.h" +#include "string.h" // COMPLIANT +#include // COMPLIANT +#include STRING_PATH // NON-COMPLIANT +// clang-format off +#include "string" ".h" // NON-COMPLIANT[False negative] +// clang-format on +#include QSTRING DOTH // NON-COMPLIANT + +// Invalid directives: +// #include string.h // NON-COMPLIANT +// #include // NON-COMPLIANT +// #include 1 + 1 // NON-COMPLIANT \ No newline at end of file diff --git a/cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.expected b/cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.expected new file mode 100644 index 000000000..14e4307e5 --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.expected @@ -0,0 +1,43 @@ +| test.cpp:36:3:36:11 | M1(X) | Macro argument 0 (with expanded value '1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:38:3:38:11 | M1(X) | Macro argument 0 (with expanded value '1 / 1') contains a critical operator '/' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:40:3:40:11 | M1(X) | Macro argument 0 (with expanded value '1 % 1') contains a critical operator '%' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:43:3:43:11 | M1(X) | Macro argument 0 (with expanded value '1 + 1') contains a critical operator '+' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:45:3:45:11 | M1(X) | Macro argument 0 (with expanded value '1 - 1') contains a critical operator '-' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:48:3:48:12 | M1(X) | Macro argument 0 (with expanded value '1 << 1') contains a critical operator '<<' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:50:3:50:12 | M1(X) | Macro argument 0 (with expanded value '1 >> 1') contains a critical operator '>>' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:53:3:53:11 | M1(X) | Macro argument 0 (with expanded value '1 < 1') contains a critical operator '<' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:55:3:55:11 | M1(X) | Macro argument 0 (with expanded value '1 > 1') contains a critical operator '>' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:57:3:57:12 | M1(X) | Macro argument 0 (with expanded value '1 <= 1') contains a critical operator '<=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:59:3:59:12 | M1(X) | Macro argument 0 (with expanded value '1 >= 1') contains a critical operator '>=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:62:3:62:12 | M1(X) | Macro argument 0 (with expanded value '1 == 1') contains a critical operator '==' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:64:3:64:12 | M1(X) | Macro argument 0 (with expanded value '1 != 1') contains a critical operator '!=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:67:3:67:11 | M1(X) | Macro argument 0 (with expanded value '1 & 1') contains a critical operator '&' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:70:3:70:11 | M1(X) | Macro argument 0 (with expanded value '1 ^ 1') contains a critical operator '^' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:73:3:73:11 | M1(X) | Macro argument 0 (with expanded value '1 \| 1') contains a critical operator '\|' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:76:3:76:12 | M1(X) | Macro argument 0 (with expanded value '1 && 1') contains a critical operator '&&' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:79:3:79:12 | M1(X) | Macro argument 0 (with expanded value '1 \|\| 1') contains a critical operator '\|\|' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:82:3:82:15 | M1(X) | Macro argument 0 (with expanded value '1 ? 1 : 1') contains a critical operator '?' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:85:3:85:12 | M1(X) | Macro argument 0 (with expanded value 'g3 = 1') contains a critical operator '=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:87:3:87:13 | M1(X) | Macro argument 0 (with expanded value 'g3 += 1') contains a critical operator '+=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:89:3:89:13 | M1(X) | Macro argument 0 (with expanded value 'g3 -= 1') contains a critical operator '-=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:91:3:91:13 | M1(X) | Macro argument 0 (with expanded value 'g3 *= 1') contains a critical operator '*=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:93:3:93:13 | M1(X) | Macro argument 0 (with expanded value 'g3 /= 1') contains a critical operator '/=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:95:3:95:13 | M1(X) | Macro argument 0 (with expanded value 'g3 %= 1') contains a critical operator '%=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:97:3:97:14 | M1(X) | Macro argument 0 (with expanded value 'g3 <<= 1') contains a critical operator '<<=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:99:3:99:14 | M1(X) | Macro argument 0 (with expanded value 'g3 >>= 1') contains a critical operator '>>=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:101:3:101:13 | M1(X) | Macro argument 0 (with expanded value 'g3 &= 1') contains a critical operator '&=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:103:3:103:13 | M1(X) | Macro argument 0 (with expanded value 'g3 ^= 1') contains a critical operator '^=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:105:3:105:13 | M1(X) | Macro argument 0 (with expanded value 'g3 \|= 1') contains a critical operator '\|=' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:138:3:138:14 | M4(PROTECTED,UNPROTECTED) | Macro argument 1 (with expanded value '1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'UNPROTECTED' is not precedence-protected with parenthesis. | test.cpp:136:1:136:59 | #define M4(PROTECTED,UNPROTECTED) (PROTECTED), UNPROTECTED | M4 | +| test.cpp:142:3:142:10 | M1(X) | Macro argument 0 (with expanded value '1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:152:3:152:13 | M1(X) | Macro argument 0 (with expanded value '1 * (1)') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:153:3:153:13 | M1(X) | Macro argument 0 (with expanded value '(1) * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:154:3:154:15 | M1(X) | Macro argument 0 (with expanded value '(1) * (1)') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:155:3:155:17 | M1(X) | Macro argument 0 (with expanded value '(1 * 1) * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:156:3:156:17 | M1(X) | Macro argument 0 (with expanded value '1 * (1 * 1)') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:11:1:11:15 | #define M1(X) X | M1 | +| test.cpp:170:3:170:15 | M6(NAME,INIT) | Macro argument 1 (with expanded value '1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'INIT' is not precedence-protected with parenthesis. | test.cpp:169:1:169:39 | #define M6(NAME,INIT) int NAME = INIT; | M6 | +| test.cpp:178:3:178:22 | M7(TYPE,NAME,INIT) | Macro argument 2 (with expanded value '= 1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'INIT' is not precedence-protected with parenthesis. | test.cpp:174:1:174:44 | #define M7(TYPE,NAME,INIT) TYPE NAME INIT; | M7 | +| test.cpp:229:3:229:19 | M10(X) | Macro argument 0 (with expanded value '"foo"[1 + 1]') contains a critical operator '+' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:197:1:201:32 | #define M10(X) (1 + 1, 1 - 1, 1 * 1, 1 / 1, 1 % 1, 1 << 1, 1 >> 1, 1 < 1, 1 > 1, 1 <= 1, 1 >= 1, 1 == 1, 1 != 1, 1 & 1, 1 ^ 1, 1 \| 1, g3 = 1, g3 += 1, g3 -= 1, g3 *= 1, g3 /= 1, g3 %= 1, g3 <<= 1, g3 >>= 1, g3 &= 1, g3 ^= 1, g3 \|= 1, 1 && 1, 1 \|\| 1, 1 ? 1 : 1, X) | M10 | +| test.cpp:235:3:235:15 | M11(X,Y) | Macro argument 0 (with expanded value '1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:234:1:234:26 | #define M11(X,Y) f3(X, Y) | M11 | +| test.cpp:239:3:240:8 | M13(X,Y) | Macro argument 0 (with expanded value '1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:238:1:238:27 | #define M13(X,Y) M12(X, Y) | M13 | +| test.cpp:253:3:254:8 | M17(X) | Macro argument 0 (with expanded value '1 * 1') contains a critical operator '*' that is not parenthesized, but macro $@ argument 'X' is not precedence-protected with parenthesis. | test.cpp:252:1:252:25 | #define M17(X) M15(X, #X) | M17 | diff --git a/cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.qlref b/cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.qlref new file mode 100644 index 000000000..ca25e5a62 --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.qlref @@ -0,0 +1 @@ +rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql \ No newline at end of file diff --git a/cpp/misra/test/rules/RULE-19-3-4/test.cpp b/cpp/misra/test/rules/RULE-19-3-4/test.cpp new file mode 100644 index 000000000..027219961 --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-3-4/test.cpp @@ -0,0 +1,263 @@ +#include +void f1(); +void f2(int x); +void f3(int x, int y); +struct S1 { + int x; +} g1, *g2; +int g3; + +void f4() { +#define M1(X) X + // No critical operators: + M1(1); // COMPLIANT + M1((1)); // COMPLIANT + M1("foo"); // COMPLIANT + M1(("foo")); // COMPLIANT + M1(f1()); // COMPLIANT + M1((f1)); // COMPLIANT + M1(*"foo"); // COMPLIANT + M1(!1); // COMPLIANT + M1(+1); // COMPLIANT + M1(-1); // COMPLIANT + M1("foo"[1]); // COMPLIANT + M1(&"foo"); // COMPLIANT + M1(g1.x); // COMPLIANT + M1(g2->x); // COMPLIANT + M1(g1.x++); // COMPLIANT + M1(++g1.x); // COMPLIANT + M1(g1.x--); // COMPLIANT + M1(--g1.x); // COMPLIANT + // M1(1, 1); -- not interpreted as a comma operator + M1((1, 1)); // COMPLIANT + M1(throw 1); // COMPLIANT + + // Level 13 operators: + M1(1 * 1); // NON-COMPLIANT + M1((1 * 1)); // COMPLIANT + M1(1 / 1); // NON-COMPLIANT + M1((1 / 1)); // COMPLIANT + M1(1 % 1); // NON-COMPLIANT + M1((1 % 1)); // COMPLIANT + // Level 12 operators: + M1(1 + 1); // NON-COMPLIANT + M1((1 + 1)); // COMPLIANT + M1(1 - 1); // NON-COMPLIANT + M1((1 - 1)); // COMPLIANT + // Level 11 operators: + M1(1 << 1); // NON-COMPLIANT + M1((1 << 1)); // COMPLIANT + M1(1 >> 1); // NON-COMPLIANT + M1((1 >> 1)); // COMPLIANT + // Level 10 operators: + M1(1 < 1); // NON-COMPLIANT + M1((1 < 1)); // COMPLIANT + M1(1 > 1); // NON-COMPLIANT + M1((1 > 1)); // COMPLIANT + M1(1 <= 1); // NON-COMPLIANT + M1((1 <= 1)); // COMPLIANT + M1(1 >= 1); // NON-COMPLIANT + M1((1 >= 1)); // COMPLIANT + // Level 9 operators: + M1(1 == 1); // NON-COMPLIANT + M1((1 == 1)); // COMPLIANT + M1(1 != 1); // NON-COMPLIANT + M1((1 != 1)); // COMPLIANT + // Level 8 operators: + M1(1 & 1); // NON-COMPLIANT + M1((1 & 1)); // COMPLIANT + // Level 7 operators: + M1(1 ^ 1); // NON-COMPLIANT + M1((1 ^ 1)); // COMPLIANT + // Level 6 operators: + M1(1 | 1); // NON-COMPLIANT + M1((1 | 1)); // COMPLIANT + // Level 5 operators: + M1(1 && 1); // NON-COMPLIANT + M1((1 && 1)); // COMPLIANT + // Level 4 operators: + M1(1 || 1); // NON-COMPLIANT + M1((1 || 1)); // COMPLIANT + // Level 3 operators: + M1(1 ? 1 : 1); // NON-COMPLIANT + M1((1 ? 1 : 1)); // COMPLIANT + // Level 2 operators: + M1(g3 = 1); // NON-COMPLIANT + M1((g3 = 1)); // COMPLIANT + M1(g3 += 1); // NON-COMPLIANT + M1((g3 += 1)); // COMPLIANT + M1(g3 -= 1); // NON-COMPLIANT + M1((g3 -= 1)); // COMPLIANT + M1(g3 *= 1); // NON-COMPLIANT + M1((g3 *= 1)); // COMPLIANT + M1(g3 /= 1); // NON-COMPLIANT + M1((g3 /= 1)); // COMPLIANT + M1(g3 %= 1); // NON-COMPLIANT + M1((g3 %= 1)); // COMPLIANT + M1(g3 <<= 1); // NON-COMPLIANT + M1((g3 <<= 1)); // COMPLIANT + M1(g3 >>= 1); // NON-COMPLIANT + M1((g3 >>= 1)); // COMPLIANT + M1(g3 &= 1); // NON-COMPLIANT + M1((g3 &= 1)); // COMPLIANT + M1(g3 ^= 1); // NON-COMPLIANT + M1((g3 ^= 1)); // COMPLIANT + M1(g3 |= 1); // NON-COMPLIANT + M1((g3 |= 1)); // COMPLIANT + // Level 1 and below are not critical operators, tested above. + +// Precedence-protected macro: +#define M2(X) (X) + M2(1 * 1); // COMPLIANT + M2(1 + 1); // COMPLIANT + M2(1 - 1); // COMPLIANT + M2(1 << 1); // COMPLIANT + M2(1 >> 1); // COMPLIANT + M2(1 < 1); // COMPLIANT + M2(1 > 1); // COMPLIANT + M2(1 <= 1); // COMPLIANT + M2(1 >= 1); // COMPLIANT + M2(1 == 1); // COMPLIANT + M2(1 != 1); // COMPLIANT + M2(1 & 1); // COMPLIANT + M2(1 ^ 1); // COMPLIANT + M2(1 | 1); // COMPLIANT + M2(1 && 1); // COMPLIANT + M2(1 || 1); // COMPLIANT + M2(1 ? 1 : 1); // COMPLIANT + +// Macro that uses the # operator: +#define M3(X) #X + M3(1 * 1); // COMPLIANT + M3(1 == 1); // COMPLIANT + M3(1 ? 1 : 1); // COMPLIANT + +// Multi-argument macro: +#define M4(PROTECTED, UNPROTECTED) (PROTECTED), UNPROTECTED + M4(1 * 1, 1); // COMPLIANT + M4(1, 1 * 1); // NON-COMPLIANT + +// Macro passed into a macro: +#define M5() 1 * 1 + M1(M5()); // NON-COMPLIANT + M1((M5())); // COMPLIANT + + // Macro with function call: + M1(f2(1 * 1)); // COMPLIANT + + // Macro with a string literal: + M1("1 * 1"); // COMPLIANT + + // Macro with lots of silly parentheses: + M1(1 * (1)); // NON-COMPLIANT + M1((1) * 1); // NON-COMPLIANT + M1((1) * (1)); // NON-COMPLIANT + M1((1 * 1) * 1); // NON-COMPLIANT + M1(1 * (1 * 1)); // NON-COMPLIANT + M1(((1 * 1))); // COMPLIANT + M1(((1) * (1))); // COMPLIANT + +// Macros with unbalanced parenthesis: +#define OP_PAREN ( +#define CL_PAREN ) + M1(OP_PAREN 1 * 1) + CL_PAREN; // COMPLIANT -- by rule description, not top level operator. + OP_PAREN M1(1 CL_PAREN * 1); // NON-COMPLIANT[False negative] -- by rule + // description, top level operator. + +// Macro expanding to a variable declaration: +#define M6(NAME, INIT) int NAME = INIT; + M6(l1, 1 * 1); // NON-COMPLIANT + M6(l2, (1 * 1)); // COMPLIANT + +// Macro expanding to a type: +#define M7(TYPE, NAME, INIT) TYPE NAME INIT; + M7(int, l3, ); // COMPLIANT + M7(std::vector, l4, ); // COMPLIANT + M7(int, l5, = 0); // COMPLIANT + M7(int, l6, = 1 * 1); // NON-COMPLIANT + M7(std::vector, l7, = std::vector()); // COMPLIANT +} + +// Macro expanding to a function declaration -- We cannot confidently analyze +// this case. +#define M8(NAME, BODY) \ + void NAME() { BODY * 2; } +M8(f5, 2 + 2) // NON-COMPLIANT[False negative] + +// Macro expanding to a class declaration -- We cannot confidently analyze this +// case. +#define M9(NAME, BODY) \ + class NAME { \ + int x = BODY * 2; \ + }; +M9(C1, 2 + 2) // NON-COMPLIANT[False negative] + +// A macro containing every critical operator: +#define M10(X) \ + (1 + 1, 1 - 1, 1 * 1, 1 / 1, 1 % 1, 1 << 1, 1 >> 1, 1 < 1, 1 > 1, 1 <= 1, \ + 1 >= 1, 1 == 1, 1 != 1, 1 & 1, 1 ^ 1, 1 | 1, g3 = 1, g3 += 1, g3 -= 1, \ + g3 *= 1, g3 /= 1, g3 %= 1, g3 <<= 1, g3 >>= 1, g3 &= 1, g3 ^= 1, g3 |= 1, \ + 1 && 1, 1 || 1, 1 ? 1 : 1, X) + +void f6() { + // No critical operators in macro arguments, handled correctly: + M10(1); // COMPLIANT -- we can tell no operators are in the macro argument + M10(g2->x); // COMPLIANT -- we can tell this isn't a lt operator + M10(g3++); // COMPLIANT -- we can tell this isn't a binary + operator + M10(g3--); // COMPLIANT -- we can tell this isn't a binary - operator + M10("1 * 1"); // COMPLIANT -- we can tell this is in a string literal + M10("*"[0] * 1); // NON-COMPLIANT[False negative] --- falsely suppressed + + // Guess that operators are unary based on the first character: + M10(*"foo"); // COMPLIANT + M10(&"foo"); // COMPLIANT + M10(-1); // COMPLIANT + M10(+1); // COMPLIANT + + // No critical operators in macro arguments, handled incorrectly: + // These falsely look to us like the critical operators are in the macro + // arguments, but they are not. + + // Compliant, we know the operators are parenthesized and cannot be + // responsible for the critical operators generated by the macro. + M10((1 * 1)); // COMPLIANT + M10((1 - 1)); // COMPLIANT + M10((1 == 1)); // COMPLIANT + + // Remaining known false positive cases + M10("foo"[1 + 1]); // NON-COMPLIANT -- by rule description, this is a top + // level operator. We also don't handle parsing matching + // brackets like this currently. But in practice, this is + // not a top level operator. + +#define M11(X, Y) f3(X, Y) + M11(1 * 1, 1); // NON-COMPLIANT by rule, but not a dangerous case. + +#define M12(X, Y) (X) + (Y) +#define M13(X, Y) M12(X, Y) + M13(1 * 1, + 1); // NON-COMPLIANT -- the rule text suggests that this is non + // compliant, though an argument could be made the other way. + // Regardless, this case is diffucult for us to detect. +#define M14(X) M1(X *X) + M14(1); // NON-COMPLIANT[False negative] -- The definition of M13 is + // non-compliant, but we don't detect the generated elements. + + // Trickier case of # operator to handle. In this case, we do not produce a + // string literal, which ordinarily is cause to suppress an alert. +#define M15(X, Y) X +#define M16(X) M15((X), #X) + M16(1 * 1); // COMPLIANT -- all expansions of X are precedence protected. +#define M17(X) M15(X, #X) + M17(1 * + 1); // NON-COMPLIANT -- not all expansions of X are precedence protected. + + // Spaces should not fool our analysis: + /* clang-format off */ +#define M18(X) ( X ) + M18(1 * 1); // COMPLIANT +#define M19(X) M15(( X ), # X) + M16(1 * 1); // COMPLIANT -- all expansions of X are precedence protected. + /* clang-format on */ +} \ No newline at end of file diff --git a/cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.expected b/cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.expected new file mode 100644 index 000000000..297622c0e --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.expected @@ -0,0 +1,2 @@ +| test.cpp:1:1:1:41 | #pragma GCC diagnostic warning "-Wformat" | Non-compliant #pragma directive 'GCC diagnostic warning "-Wformat"'. | +| test.cpp:2:1:2:36 | #define TODO(X) _Pragma("TODO: " #X) | Non-compliant _Pragma operator used: '_Pragma("TODO: " #X)'. | diff --git a/cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.qlref b/cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.qlref new file mode 100644 index 000000000..107efc652 --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.qlref @@ -0,0 +1 @@ +rules/RULE-19-6-1/DisallowedUseOfPragma.ql \ No newline at end of file diff --git a/cpp/misra/test/rules/RULE-19-6-1/test.cpp b/cpp/misra/test/rules/RULE-19-6-1/test.cpp new file mode 100644 index 000000000..ef65e34e0 --- /dev/null +++ b/cpp/misra/test/rules/RULE-19-6-1/test.cpp @@ -0,0 +1,4 @@ +#pragma GCC diagnostic warning "-Wformat" // NON-COMPLIANT +#define TODO(X) _Pragma("TODO: " #X) // NON-COMPLIANT +void not_Pragma(); +#define NOT_PRAGMA not_Pragma() // COMPLIANT diff --git a/rule_packages/cpp/Preprocessor2.json b/rule_packages/cpp/Preprocessor2.json new file mode 100644 index 000000000..ed3d327b8 --- /dev/null +++ b/rule_packages/cpp/Preprocessor2.json @@ -0,0 +1,72 @@ +{ + "MISRA-C++-2023": { + "RULE-19-2-2": { + "properties": { + "enforcement": "decidable", + "obligation": "required" + }, + "queries": [ + { + "description": "Include directives shall only use the or \"filename\" forms.", + "kind": "problem", + "name": "The #include directive shall be followed by either a or \"filename\" sequence", + "precision": "very-high", + "severity": "warning", + "short_name": "InvalidIncludeDirective", + "tags": [ + "scope/single-translation-unit", + "maintainability", + "readability" + ] + } + ], + "title": "The #include directive shall be followed by either a or \"filename\" sequence" + }, + "RULE-19-3-4": { + "properties": { + "enforcement": "decidable", + "obligation": "required" + }, + "queries": [ + { + "description": "Expanded macro arguments shall be enclosed in parentheses to ensure the resulting expressions have the expected precedence and order of operations.", + "kind": "problem", + "name": "Parentheses shall be used to ensure macro arguments are expanded appropriately", + "precision": "high", + "severity": "error", + "short_name": "UnparenthesizedMacroArgument", + "tags": [ + "scope/single-translation-unit", + "correctness", + "maintainability" + ], + "implementation_scope": { + "description": "A mixture of textual and syntactic analysis is used to identify the problem, with positional cues being required to infer unary *, &, +, or - and semantic clues suggesting when <, >, * may refer to a type. The presence of string literals and non-critical operators ++, --, -> or unmatched parenthesis may abort the analysis. Safe cases not exempted in the rule text such as comma-separated arguments or brackets are not exempted by this query." + } + } + ], + "title": "Parentheses shall be used to ensure macro arguments are expanded appropriately" + }, + "RULE-19-6-1": { + "properties": { + "enforcement": "decidable", + "obligation": "advisory" + }, + "queries": [ + { + "description": "Preprocessor pragma directives are implementation-defined, and should not be used to maintain code portability.", + "kind": "problem", + "name": "The #pragma directive and the _Pragma operator should not be used", + "precision": "very-high", + "severity": "warning", + "short_name": "DisallowedUseOfPragma", + "tags": [ + "scope/single-translation-unit", + "maintainability" + ] + } + ], + "title": "The #pragma directive and the _Pragma operator should not be used" + } + } +} \ No newline at end of file