From a653a58dc994b7d7ab40129e3ea1e556f7655d5a Mon Sep 17 00:00:00 2001 From: Mike Fairhurst Date: Wed, 14 May 2025 10:30:27 -0700 Subject: [PATCH 1/7] Implement package preprocessor2 --- cpp/common/src/codingstandards/cpp/Macro.qll | 43 +++ .../cpp/MatchingParenthesis.qll | 2 +- .../cpp/exclusions/cpp/Preprocessor2.qll | 61 ++++ .../cpp/exclusions/cpp/RuleMetadata.qll | 3 + .../RULE-19-2-2/InvalidIncludeDirective.ql | 24 ++ .../UnparenthesizedMacroArgument.ql | 187 +++++++++++++ .../RULE-19-6-1/DisallowedUseOfPragma.ql | 32 +++ .../InvalidIncludeDirective.expected | 2 + .../RULE-19-2-2/InvalidIncludeDirective.qlref | 1 + cpp/misra/test/rules/RULE-19-2-2/test.cpp | 15 + .../UnparenthesizedMacroArgument.expected | 43 +++ .../UnparenthesizedMacroArgument.qlref | 1 + cpp/misra/test/rules/RULE-19-3-4/test.cpp | 263 ++++++++++++++++++ .../DisallowedUseOfPragma.expected | 2 + .../RULE-19-6-1/DisallowedUseOfPragma.qlref | 1 + cpp/misra/test/rules/RULE-19-6-1/test.cpp | 4 + rule_packages/cpp/Preprocessor2.json | 69 +++++ 17 files changed, 752 insertions(+), 1 deletion(-) create mode 100644 cpp/common/src/codingstandards/cpp/exclusions/cpp/Preprocessor2.qll create mode 100644 cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql create mode 100644 cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql create mode 100644 cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql create mode 100644 cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.expected create mode 100644 cpp/misra/test/rules/RULE-19-2-2/InvalidIncludeDirective.qlref create mode 100644 cpp/misra/test/rules/RULE-19-2-2/test.cpp create mode 100644 cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.expected create mode 100644 cpp/misra/test/rules/RULE-19-3-4/UnparenthesizedMacroArgument.qlref create mode 100644 cpp/misra/test/rules/RULE-19-3-4/test.cpp create mode 100644 cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.expected create mode 100644 cpp/misra/test/rules/RULE-19-6-1/DisallowedUseOfPragma.qlref create mode 100644 cpp/misra/test/rules/RULE-19-6-1/test.cpp create mode 100644 rule_packages/cpp/Preprocessor2.json diff --git a/cpp/common/src/codingstandards/cpp/Macro.qll b/cpp/common/src/codingstandards/cpp/Macro.qll index 6514e957fb..8d821ffaf2 100644 --- a/cpp/common/src/codingstandards/cpp/Macro.qll +++ b/cpp/common/src/codingstandards/cpp/Macro.qll @@ -19,6 +19,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 0000000000..0993b099ff --- /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 e68d714243..341e6986c0 100644 --- a/cpp/common/src/codingstandards/cpp/exclusions/cpp/RuleMetadata.qll +++ b/cpp/common/src/codingstandards/cpp/exclusions/cpp/RuleMetadata.qll @@ -41,6 +41,7 @@ import OrderOfEvaluation import OutOfBounds import Pointers import Preprocessor +import Preprocessor2 import Representation import Scope import SideEffects1 @@ -96,6 +97,7 @@ newtype TCPPQuery = TOutOfBoundsPackageQuery(OutOfBoundsQuery q) or TPointersPackageQuery(PointersQuery q) or TPreprocessorPackageQuery(PreprocessorQuery q) or + TPreprocessor2PackageQuery(Preprocessor2Query q) or TRepresentationPackageQuery(RepresentationQuery q) or TScopePackageQuery(ScopeQuery q) or TSideEffects1PackageQuery(SideEffects1Query q) or @@ -151,6 +153,7 @@ predicate isQueryMetadata(Query query, string queryId, string ruleId, string cat isOutOfBoundsQueryMetadata(query, queryId, ruleId, category) or isPointersQueryMetadata(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 0000000000..d0867582ba --- /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 error + * @tags external/misra/id/rule-19-2-2 + * scope/single-translation-unit + * maintainability + * correctness + * 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 0000000000..9f97ec9c0a --- /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 very-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 0000000000..8a61d755e4 --- /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 error + * @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 0000000000..4e85513b0e --- /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 0000000000..6268670c9b --- /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 0000000000..b1788458ec --- /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 0000000000..14e4307e5c --- /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 0000000000..ca25e5a62a --- /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 0000000000..027219961d --- /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 0000000000..297622c0e0 --- /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 0000000000..107efc6520 --- /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 0000000000..ef65e34e0f --- /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 0000000000..a84f3eff5f --- /dev/null +++ b/rule_packages/cpp/Preprocessor2.json @@ -0,0 +1,69 @@ +{ + "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": "error", + "short_name": "InvalidIncludeDirective", + "tags": [ + "scope/single-translation-unit", + "maintainability", + "correctness" + ] + } + ], + "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": "very-high", + "severity": "error", + "short_name": "UnparenthesizedMacroArgument", + "tags": [ + "scope/single-translation-unit", + "correctness", + "maintainability" + ] + } + ], + "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": "error", + "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 From a020c0fdca1d971a502ae3c1b87128177d8febf6 Mon Sep 17 00:00:00 2001 From: Mike Fairhurst Date: Wed, 14 May 2025 11:14:53 -0700 Subject: [PATCH 2/7] Update query metadata --- .../rules/RULE-19-2-2/InvalidIncludeDirective.ql | 4 ++-- .../RULE-19-3-4/UnparenthesizedMacroArgument.ql | 2 +- .../src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql | 2 +- rule_packages/cpp/Preprocessor2.json | 13 ++++++++----- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql b/cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql index d0867582ba..2e49b34d43 100644 --- a/cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql +++ b/cpp/misra/src/rules/RULE-19-2-2/InvalidIncludeDirective.ql @@ -4,11 +4,11 @@ * @description Include directives shall only use the or "filename" forms. * @kind problem * @precision very-high - * @problem.severity error + * @problem.severity warning * @tags external/misra/id/rule-19-2-2 * scope/single-translation-unit * maintainability - * correctness + * readability * external/misra/enforcement/decidable * external/misra/obligation/required */ diff --git a/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql index 9f97ec9c0a..09cada7b08 100644 --- a/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql +++ b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql @@ -4,7 +4,7 @@ * @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 very-high + * @precision high * @problem.severity error * @tags external/misra/id/rule-19-3-4 * scope/single-translation-unit diff --git a/cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql b/cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql index 8a61d755e4..0e07ee02f4 100644 --- a/cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql +++ b/cpp/misra/src/rules/RULE-19-6-1/DisallowedUseOfPragma.ql @@ -5,7 +5,7 @@ * maintain code portability. * @kind problem * @precision very-high - * @problem.severity error + * @problem.severity warning * @tags external/misra/id/rule-19-6-1 * scope/single-translation-unit * maintainability diff --git a/rule_packages/cpp/Preprocessor2.json b/rule_packages/cpp/Preprocessor2.json index a84f3eff5f..ed3d327b88 100644 --- a/rule_packages/cpp/Preprocessor2.json +++ b/rule_packages/cpp/Preprocessor2.json @@ -11,12 +11,12 @@ "kind": "problem", "name": "The #include directive shall be followed by either a or \"filename\" sequence", "precision": "very-high", - "severity": "error", + "severity": "warning", "short_name": "InvalidIncludeDirective", "tags": [ "scope/single-translation-unit", "maintainability", - "correctness" + "readability" ] } ], @@ -32,14 +32,17 @@ "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": "very-high", + "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" @@ -55,7 +58,7 @@ "kind": "problem", "name": "The #pragma directive and the _Pragma operator should not be used", "precision": "very-high", - "severity": "error", + "severity": "warning", "short_name": "DisallowedUseOfPragma", "tags": [ "scope/single-translation-unit", From 36eee163188ffc00c01dd4b9288b02dc4be41d01 Mon Sep 17 00:00:00 2001 From: Mike Fairhurst Date: Sun, 24 Aug 2025 11:00:57 -0700 Subject: [PATCH 3/7] Update to use features from codeql-qtil --- .../cpp/util/CondensedList.qll | 105 ------------------ .../src/codingstandards/cpp/util/Pair.qll | 21 ---- cpp/misra/src/codeql-pack.lock.yml | 2 + cpp/misra/src/qlpack.yml | 1 + .../UndefOfMacroNotDefinedInFile.ql | 49 ++++---- cpp/misra/test/codeql-pack.lock.yml | 2 + 6 files changed, 25 insertions(+), 155 deletions(-) delete mode 100644 cpp/common/src/codingstandards/cpp/util/CondensedList.qll delete mode 100644 cpp/common/src/codingstandards/cpp/util/Pair.qll diff --git a/cpp/common/src/codingstandards/cpp/util/CondensedList.qll b/cpp/common/src/codingstandards/cpp/util/CondensedList.qll deleted file mode 100644 index bda0177bff..0000000000 --- a/cpp/common/src/codingstandards/cpp/util/CondensedList.qll +++ /dev/null @@ -1,105 +0,0 @@ -private import codeql.util.DenseRank - -/** - * Describes how to construct a condensed list from sparse but orderable data, and how that data - * should be connected, with one such list per specified division. - */ -signature module CondensedListSig { - /** - * The division specifies which items are connected into lists, with one list per division. - * - * For instance, if connecting variables defined in a file, the division will be the file. - */ - class Division; - - /** - * The class of the items to be condensed into lists. - * - * For instance, when connecting variables defined in a file, the items are the variables. - */ - class Item { - string toString(); - } - - /** - * The index specifies the order of the items in the condensed list, and may be sparse (have - * gaps). - * - * For instance, if connecting variables defined in a file, the index will be the line number of - * the variable in the file. - * - * The sparse index (which may have gaps) is used to determine the ordering of the items in the - * condensed list. Once the condensed list is created, the items in the list will automatically be - * assigned a dense index (which has no gaps). - * - * There must be no duplicate indices for the same division for correctness. - */ - int getSparseIndex(Division d, Item l); -} - -/** - * A module to take orderable data (which may not be continuous) and condense it into one or more - * dense lists, with one such list per specified division. - * - * To instantiate this module, you need to provide a `CondensedListSig` module that - * specifies the spare index and division of the items to be connected. - * - * For instance, to create a condensed list of variables defined in every file, you can - * create a `CondensedListSig` module that specifies the file as the division and - * the line number as the sparse index. - * - * ```ql - * module ConfigFileListConfig { - * class Division = File; - * class Item = Variable; - * int getSparseIndex(File file, Variable var) { - * file = var.getLocation().getFile() and - * var.getLocation().getStartLine() - * } - * } - * - * import Condense - * - * from Condense::ListEntry l - * select l, l.getItem(), l.getDenseIndex(), l.getNext(), l.getPrev(), - * ``` - */ -module Condense { - newtype TList = - THead(Config::Item l, Config::Division t) { denseRank(t, l) = 1 } or - TCons(ListEntry prev, Config::Item l) { - prev.getDenseIndex() = denseRank(prev.getDivision(), l) - 1 - } - - private module DenseRankConfig implements DenseRankInputSig2 { - class Ranked = Config::Item; - - class C = Config::Division; - - predicate getRank = Config::getSparseIndex/2; - } - - private import DenseRank2 - - class ListEntry extends TList { - Config::Division getDivision() { - this = THead(_, result) - or - exists(ListEntry prev | this = TCons(prev, _) and result = prev.getDivision()) - } - - string toString() { result = getItem().toString() + " [index " + getDenseIndex() + "]" } - - Config::Item getItem() { - this = THead(result, _) - or - this = TCons(_, result) - } - - int getDenseIndex() { result = denseRank(getDivision(), getItem()) } - - ListEntry getPrev() { this = TCons(result, _) } - - ListEntry getNext() { result.getPrev() = this } - } -} diff --git a/cpp/common/src/codingstandards/cpp/util/Pair.qll b/cpp/common/src/codingstandards/cpp/util/Pair.qll deleted file mode 100644 index a0c879ab93..0000000000 --- a/cpp/common/src/codingstandards/cpp/util/Pair.qll +++ /dev/null @@ -1,21 +0,0 @@ -bindingset[this] -signature class ItemSig { - bindingset[this] - string toString(); -} - -module Pair { - signature predicate pred(A a, B b); - - module Where { - private newtype TAll = TSome(A a, B b) { ctor(a, b) } - - class Pair extends TAll { - A getFirst() { this = TSome(result, _) } - - B getSecond() { this = TSome(_, result) } - - string toString() { result = getFirst().toString() + ", " + getSecond().toString() } - } - } -} diff --git a/cpp/misra/src/codeql-pack.lock.yml b/cpp/misra/src/codeql-pack.lock.yml index ab9a39f9c1..5539437098 100644 --- a/cpp/misra/src/codeql-pack.lock.yml +++ b/cpp/misra/src/codeql-pack.lock.yml @@ -1,6 +1,8 @@ --- lockVersion: 1.0.0 dependencies: + advanced-security/qtil: + version: 0.0.1 codeql/cpp-all: version: 2.1.1 codeql/dataflow: diff --git a/cpp/misra/src/qlpack.yml b/cpp/misra/src/qlpack.yml index 27e0893ed6..38deea034e 100644 --- a/cpp/misra/src/qlpack.yml +++ b/cpp/misra/src/qlpack.yml @@ -6,3 +6,4 @@ license: MIT dependencies: codeql/common-cpp-coding-standards: '*' codeql/cpp-all: 2.1.1 + advanced-security/qtil: 0.0.1 diff --git a/cpp/misra/src/rules/RULE-19-0-4/UndefOfMacroNotDefinedInFile.ql b/cpp/misra/src/rules/RULE-19-0-4/UndefOfMacroNotDefinedInFile.ql index a8121f2da5..45ff48f1c1 100644 --- a/cpp/misra/src/rules/RULE-19-0-4/UndefOfMacroNotDefinedInFile.ql +++ b/cpp/misra/src/rules/RULE-19-0-4/UndefOfMacroNotDefinedInFile.ql @@ -16,18 +16,15 @@ import cpp import codingstandards.cpp.misra -import codingstandards.cpp.util.CondensedList -import codingstandards.cpp.util.Pair +import qtil.Qtil class DefOrUndef extends PreprocessorDirective { - string name; + DefOrUndef() { this instanceof PreprocessorUndef or this instanceof Macro } - DefOrUndef() { - name = this.(PreprocessorUndef).getName() or - name = this.(Macro).getName() + string getName() { + result = this.(PreprocessorUndef).getName() or + result = this.(Macro).getName() } - - string getName() { result = name } } predicate relevantNameAndFile(string name, File file) { @@ -37,30 +34,24 @@ predicate relevantNameAndFile(string name, File file) { ) } -class StringFilePair = Pair::Where::Pair; - -module DefUndefListConfig implements CondensedListSig { - class Division = StringFilePair; +class StringFilePair = Qtil::Pair::Pair; - class Item = DefOrUndef; +/** + * Defs and undefs ordered by location, grouped by name and file. + */ +class OrderedDefOrUndef extends Qtil::Ordered::GroupBy::Type { + override int getOrder() { result = getLocation().getStartLine() } - int getSparseIndex(StringFilePair division, DefOrUndef directive) { - directive.getName() = division.getFirst() and - directive.getFile() = division.getSecond() and - result = directive.getLocation().getStartLine() + override StringFilePair getGroup() { + result.getFirst() = getName() and result.getSecond() = getFile() } } -class ListEntry = Condense::ListEntry; - -from PreprocessorUndef undef, ListEntry defUndefListEntry +from OrderedDefOrUndef defOrUndef where - not isExcluded(undef, PreprocessorPackage::undefOfMacroNotDefinedInFileQuery()) and - // There exists a def or undef for a given name and file, and it is an #undef - undef = defUndefListEntry.getItem() and - // Exclude cases where the previous def or undef with the same name in the same file is a #define - not exists(ListEntry prev | - prev = defUndefListEntry.getPrev() and - prev.getItem() instanceof Macro - ) -select undef, "Undef of name '" + undef.getName() + "' not defined in the same file." + not isExcluded(defOrUndef, PreprocessorPackage::undefOfMacroNotDefinedInFileQuery()) and + // There exists an #undef for a given name and file + defOrUndef instanceof PreprocessorUndef and + // A previous def or undef of this name must exist in this file, and it must be a #define + not defOrUndef.getPrevious() instanceof Macro +select defOrUndef, "Undef of name '" + defOrUndef.getName() + "' not defined in the same file." diff --git a/cpp/misra/test/codeql-pack.lock.yml b/cpp/misra/test/codeql-pack.lock.yml index ab9a39f9c1..5539437098 100644 --- a/cpp/misra/test/codeql-pack.lock.yml +++ b/cpp/misra/test/codeql-pack.lock.yml @@ -1,6 +1,8 @@ --- lockVersion: 1.0.0 dependencies: + advanced-security/qtil: + version: 0.0.1 codeql/cpp-all: version: 2.1.1 codeql/dataflow: From 5b9b524630c6dd61b995d1051f83c22bb4c1a106 Mon Sep 17 00:00:00 2001 From: Michael R Fairhurst Date: Sun, 1 Mar 2026 08:50:02 -0800 Subject: [PATCH 4/7] Update cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql index 09cada7b08..f0c1ec0371 100644 --- a/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql +++ b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql @@ -66,7 +66,7 @@ class CriticalOperatorExpr extends Expr { * 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 + * This class is used in two passes. Firstly, with `hasRiskyParameter`, to find the macro parameters * 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. From 0e7ed11f2049aedb672650bbf7c1a0e3080619ac Mon Sep 17 00:00:00 2001 From: Michael R Fairhurst Date: Sun, 1 Mar 2026 08:50:18 -0800 Subject: [PATCH 5/7] Update cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql index f0c1ec0371..11ffff46df 100644 --- a/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql +++ b/cpp/misra/src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql @@ -68,7 +68,7 @@ class CriticalOperatorExpr extends Expr { * * This class is used in two passes. Firstly, with `hasRiskyParameter`, to find the macro parameters * 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 + * risky parameters and attempt to match the produced AST to an unparenthesized occurrence of that * operator in the argument text. * * For a given macro invocation to be considered risky, it must From 601073211ba9380052ba711ef291a215d3101ce1 Mon Sep 17 00:00:00 2001 From: Michael R Fairhurst Date: Sun, 1 Mar 2026 08:50:31 -0800 Subject: [PATCH 6/7] Update cpp/misra/test/rules/RULE-19-3-4/test.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cpp/misra/test/rules/RULE-19-3-4/test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/misra/test/rules/RULE-19-3-4/test.cpp b/cpp/misra/test/rules/RULE-19-3-4/test.cpp index 027219961d..09cffa6962 100644 --- a/cpp/misra/test/rules/RULE-19-3-4/test.cpp +++ b/cpp/misra/test/rules/RULE-19-3-4/test.cpp @@ -239,7 +239,7 @@ void f6() { 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. + // Regardless, this case is difficult 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. From 5f10e7c73f7223f4998d50f0bae55603172b8903 Mon Sep 17 00:00:00 2001 From: Mike Fairhurst Date: Sun, 1 Mar 2026 09:18:46 -0800 Subject: [PATCH 7/7] copilot feedback --- cpp/common/src/codingstandards/cpp/Macro.qll | 2 +- .../src/rules/RULE-19-3-4/UnparenthesizedMacroArgument.ql | 6 ++---- cpp/misra/test/rules/RULE-19-3-4/test.cpp | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cpp/common/src/codingstandards/cpp/Macro.qll b/cpp/common/src/codingstandards/cpp/Macro.qll index 4b71331ede..04002dde69 100644 --- a/cpp/common/src/codingstandards/cpp/Macro.qll +++ b/cpp/common/src/codingstandards/cpp/Macro.qll @@ -61,7 +61,7 @@ class FunctionLikeMacro extends Macro { noAfterParen = "(?!\\s*\\))" and // Parameter at word boundary in optional whitespace paramMatch = "\\s*\\b" + parameter + "\\b\\s*" and - // A parameter is ##'d if it is preceded or followed by the # operator. + // A parameter is #'d if it is preceded or followed by the # or ## operators. notHashed = "(?", "++", or "--" - * operators. + * - the operator should not be found inside a "->", "++", or "--" operator. * * The results of this predicate should be flagged by the query. */ @@ -141,7 +139,7 @@ class RiskyMacroInvocation extends MacroInvocation { int opIndex | parsedRoot.getInputString() = value and - (topLevelText.getParent() = parsedRoot or topLevelText = parsedRoot) and + parsedRoot = topLevelText.getParent() and text = topLevelText.getText().trim() and opExpr = getAGeneratedElement() and operator = opExpr.getOperator() and diff --git a/cpp/misra/test/rules/RULE-19-3-4/test.cpp b/cpp/misra/test/rules/RULE-19-3-4/test.cpp index 09cffa6962..d45d56b7da 100644 --- a/cpp/misra/test/rules/RULE-19-3-4/test.cpp +++ b/cpp/misra/test/rules/RULE-19-3-4/test.cpp @@ -241,7 +241,7 @@ void f6() { // compliant, though an argument could be made the other way. // Regardless, this case is difficult for us to detect. #define M14(X) M1(X *X) - M14(1); // NON-COMPLIANT[False negative] -- The definition of M13 is + M14(1); // NON-COMPLIANT[False negative] -- The definition of M14 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 @@ -258,6 +258,6 @@ void f6() { #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. + M19(1 * 1); // COMPLIANT -- all expansions of X are precedence protected. /* clang-format on */ -} \ No newline at end of file +}