From 8f0ea6147a4de49bebdc6062857486871a881d5b Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Thu, 13 Aug 2026 16:16:36 +0530 Subject: [PATCH 1/3] C/C++: Detect ambiguous assignment of comparison results Original-patch-by: Richard Yao --- .../cpp-code-quality-extended.qls.expected | 2 +- .../query-suite/cpp-code-quality.qls.expected | 2 +- .../cpp-security-and-quality.qls.expected | 1 + .../AmbiguousAssignmentOfComparison.cpp | 12 + .../AmbiguousAssignmentOfComparison.qhelp | 31 ++ .../AmbiguousAssignmentOfComparison.ql | 56 ++++ ...8-13-ambiguous-assignment-of-comparison.md | 5 + .../AmbiguousAssignmentOfComparison.expected | 24 ++ .../AmbiguousAssignmentOfComparison.qlref | 2 + .../AmbiguousAssignmentOfComparison/test.c | 91 ++++++ .../AmbiguousAssignmentOfComparison/test.cpp | 300 ++++++++++++++++++ 11 files changed, 524 insertions(+), 2 deletions(-) create mode 100644 cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.cpp create mode 100644 cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp create mode 100644 cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql create mode 100644 cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md create mode 100644 cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected create mode 100644 cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.qlref create mode 100644 cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c create mode 100644 cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp diff --git a/cpp/ql/integration-tests/query-suite/cpp-code-quality-extended.qls.expected b/cpp/ql/integration-tests/query-suite/cpp-code-quality-extended.qls.expected index 8b137891791f..85790a23a836 100644 --- a/cpp/ql/integration-tests/query-suite/cpp-code-quality-extended.qls.expected +++ b/cpp/ql/integration-tests/query-suite/cpp-code-quality-extended.qls.expected @@ -1 +1 @@ - +ql/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql diff --git a/cpp/ql/integration-tests/query-suite/cpp-code-quality.qls.expected b/cpp/ql/integration-tests/query-suite/cpp-code-quality.qls.expected index 8b137891791f..85790a23a836 100644 --- a/cpp/ql/integration-tests/query-suite/cpp-code-quality.qls.expected +++ b/cpp/ql/integration-tests/query-suite/cpp-code-quality.qls.expected @@ -1 +1 @@ - +ql/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql diff --git a/cpp/ql/integration-tests/query-suite/cpp-security-and-quality.qls.expected b/cpp/ql/integration-tests/query-suite/cpp-security-and-quality.qls.expected index cb4e5f7b305a..17ebac8ee50c 100644 --- a/cpp/ql/integration-tests/query-suite/cpp-security-and-quality.qls.expected +++ b/cpp/ql/integration-tests/query-suite/cpp-security-and-quality.qls.expected @@ -65,6 +65,7 @@ ql/cpp/ql/src/Likely Bugs/InconsistentCheckReturnNull.ql ql/cpp/ql/src/Likely Bugs/Leap Year/Adding365DaysPerYear.ql ql/cpp/ql/src/Likely Bugs/Leap Year/UncheckedLeapYearAfterYearModification.ql ql/cpp/ql/src/Likely Bugs/Leap Year/UncheckedReturnValueForTimeFunctions.ql +ql/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql ql/cpp/ql/src/Likely Bugs/Likely Typos/AssignWhereCompareMeant.ql ql/cpp/ql/src/Likely Bugs/Likely Typos/CompareWhereAssignMeant.ql ql/cpp/ql/src/Likely Bugs/Likely Typos/DubiousNullCheck.ql diff --git a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.cpp b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.cpp new file mode 100644 index 000000000000..c99ee284346c --- /dev/null +++ b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.cpp @@ -0,0 +1,12 @@ +int read_status(); + +int check_status() { + int status; + if (status = read_status() < 0) // BAD: assigns the comparison result. + return status; + + if ((status = read_status()) < 0) // GOOD: assigns first, then compares. + return status; + + return 0; +} diff --git a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp new file mode 100644 index 000000000000..53c001212001 --- /dev/null +++ b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp @@ -0,0 +1,31 @@ + + + + +

Assignment operators have lower precedence than comparison operators. For example, +status = read_status() < 0 assigns the comparison result (zero or one) to +status. This can be unintended when the programmer meant to assign the return value +first and then compare it with zero.

+
+ + +

Use parentheses to make the intended operation order explicit. To assign first and compare the +assigned value, parenthesize the assignment. To intentionally assign the comparison result, +parenthesize the comparison. An explicit cast around the comparison also makes that order clear.

+
+ + +

In the first condition, status receives either zero or one instead of the value +returned by read_status. The second condition explicitly performs the assignment +before the comparison.

+ +
+ + +
  • SEI CERT C Coding Standard: EXP00-C. Use parentheses for precedence of operation.
  • +
  • C++ reference: Operator precedence.
  • +
    + +
    diff --git a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql new file mode 100644 index 000000000000..be3007ffa038 --- /dev/null +++ b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql @@ -0,0 +1,56 @@ +/** + * @name Ambiguous assignment of comparison in condition + * @description Assigning the result of an unparenthesized comparison in a condition may indicate + * that the assignment and comparison are grouped incorrectly. + * @kind problem + * @problem.severity warning + * @precision high + * @id cpp/ambiguous-assignment-of-comparison + * @tags quality + * reliability + * correctness + * external/cwe/cwe-783 + */ + +import cpp + +/** Gets a condition that controls branching. */ +private Expr getACondition() { + result = any(IfStmt s).getCondition() + or + result = any(Loop s).getCondition() + or + result = any(ConditionalExpr e).getCondition() +} + +/** + * Holds if `assignment` occurs within a condition that controls branching. + * + * This includes nested expressions, such as function arguments and either operand of a comma + * expression, because the ambiguous syntax still occurs within the condition. + */ +private predicate occursInCondition(Assignment assignment) { + assignment.getParent*() = getACondition() +} + +/** + * Holds if `comparison` is explicitly grouped using parentheses or an explicit cast. + */ +private predicate isExplicitlyGrouped(ComparisonOperation comparison) { + comparison.isParenthesised() + or + exists(Cast cast | cast = comparison.getConversion+() and not cast.isImplicit()) +} + +from Assignment assignment, ComparisonOperation comparison +where + assignment.getRValue() = comparison and + not isExplicitlyGrouped(comparison) and + occursInCondition(assignment) and + // Assigning a comparison result to a Boolean is normally intentional. + not assignment.getLValue().getUnspecifiedType() instanceof BoolType and + not assignment.isUnevaluated() and + not assignment.isFromUninstantiatedTemplate(_) +select assignment, + "The '" + assignment.getOperator() + + "' operation assigns the result of an unparenthesized comparison used in a condition." diff --git a/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md b/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md new file mode 100644 index 000000000000..ae9e2c5d3823 --- /dev/null +++ b/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md @@ -0,0 +1,5 @@ +--- +category: newQuery +--- +* Added a new query, `cpp/ambiguous-assignment-of-comparison`, to detect assignments of + unparenthesized comparison results in conditions. diff --git a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected new file mode 100644 index 000000000000..9075768e9f98 --- /dev/null +++ b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected @@ -0,0 +1,24 @@ +| test.c:6:8:6:31 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.c:13:30:13:54 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.c:20:8:20:33 | ... /= ... | The '/=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.c:27:8:27:33 | ... %= ... | The '%=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.c:34:8:34:32 | ... \|= ... | The '\|=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.c:41:8:41:33 | ... >>= ... | The '>>=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.c:51:3:51:26 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:8:8:8:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:15:11:15:34 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:22:7:22:29 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:29:11:29:40 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:36:8:36:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:43:11:43:33 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:48:8:48:33 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:55:8:55:32 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:64:13:64:35 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:70:29:70:51 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:77:8:77:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:84:8:84:38 | ... <<= ... | The '<<=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:91:9:91:31 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:101:3:101:20 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:253:8:253:24 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:280:16:280:38 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.cpp:287:8:287:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | diff --git a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.qlref b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.qlref new file mode 100644 index 000000000000..ffa4d7fb05d8 --- /dev/null +++ b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.qlref @@ -0,0 +1,2 @@ +query: Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql diff --git a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c new file mode 100644 index 000000000000..ee78980b3672 --- /dev/null +++ b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c @@ -0,0 +1,91 @@ +int read_value(void); +int read_other_value(void); + +int c_direct_condition(void) { + int value; + if ((value = read_value() < 0)) // $ Alert // BAD + return value; + return 0; +} + +int c_logical_condition(void) { + int value; + if (read_other_value() && (value = read_value() >= 0)) // $ Alert // BAD + return value; + return 0; +} + +int c_compound_divide(void) { + int value = 8; + if ((value /= read_value() != 0)) // $ Alert // BAD + return value; + return 0; +} + +int c_compound_remainder(void) { + int value = 8; + if ((value %= read_value() != 0)) // $ Alert // BAD + return value; + return 0; +} + +int c_compound_bitwise_or(void) { + int value = 0; + if ((value |= read_value() > 0)) // $ Alert // BAD + return value; + return 0; +} + +int c_compound_right_shift(void) { + int value = 8; + if ((value >>= read_value() > 0)) // $ Alert // BAD + return value; + return 0; +} + +#define C_AMBIGUOUS_CHECK(VALUE) \ + if (((VALUE) = read_value() < 0)) return (VALUE) + +int c_macro_condition(void) { + int value; + C_AMBIGUOUS_CHECK(value); // $ Alert // BAD + return 0; +} + +int c_explicit_assign_then_compare(void) { + int value; + if ((value = read_value()) < 0) // GOOD + return value; + return 0; +} + +int c_explicit_compare_then_assign(void) { + int value; + if ((value = (read_value() < 0))) // GOOD + return value; + return 0; +} + +int c_explicit_cast_of_comparison(void) { + int value; + if ((value = (int)(read_value() < 0))) // GOOD: The cast explicitly groups the comparison. + return value; + return 0; +} + +int c_boolean_result_assignment(void) { + _Bool negative; + if ((negative = read_value() < 0)) // GOOD: Assigning a comparison result to a Boolean is natural. + return negative; + return 0; +} + +int c_switch_expression(void) { + int value; + switch (value = read_value() < 0) { // GOOD: This query only covers branching conditions. + case 0: + return value; + default: + return 0; + } +} diff --git a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp new file mode 100644 index 000000000000..8082c7925f92 --- /dev/null +++ b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp @@ -0,0 +1,300 @@ +int get_value(); +int get_other_value(); +void *get_pointer(); +bool identity(bool value); + +int direct_if() { + int value; + if ((value = get_value() < 0)) // $ Alert // BAD + return value; + return 0; +} + +int direct_while() { + int value; + while ((value = get_value() != 0)) // $ Alert // BAD + return value; + return 0; +} + +int without_outer_parentheses() { + int value; + if (value = get_value() < 0) // $ Alert // BAD + return value; + return 0; +} + +int for_condition() { + int value; + for (; (value = get_other_value() <= 0);) // $ Alert // BAD + return value; + return 0; +} + +int logical_and() { + int value; + if ((value = get_value() < 0) && get_other_value()) // $ Alert // BAD + return value; + return 0; +} + +int ternary_condition() { + int value; + return (value = get_value() > 0) ? value : 0; // $ Alert // BAD +} + +int pointer_comparison() { + int value; + if ((value = get_pointer() == 0)) // $ Alert // BAD + return value; + return 0; +} + +int parenthesized_operand_only() { + int value; + if ((value = (get_value()) < 0)) // $ Alert // BAD + return value; + return 0; +} + +int do_while_condition() { + int value = 0; + do { + value++; + } while ((value = get_value() < 0)); // $ Alert // BAD + return value; +} + +int logical_or() { + int value; + if (get_other_value() || (value = get_value() > 0)) // $ Alert // BAD + return value; + return 0; +} + +int nested_comparison() { + int value; + if ((value = get_value() < 0) == 1) // $ Alert // BAD + return value; + return 0; +} + +int compound_shift() { + int value = 1; + if ((value <<= get_other_value() > 0)) // $ Alert // BAD + return value; + return 0; +} + +int under_logical_not() { + int value; + if (!(value = get_value() < 0)) // $ Alert // BAD + return value; + return 0; +} + +#define CHECK_VALUE(VALUE) \ + if (((VALUE) = get_value() < 0)) return (VALUE) + +int macro_condition() { + int value; + CHECK_VALUE(value); // $ Alert // BAD + return 0; +} + +int explicit_assign_then_compare() { + int value; + if ((value = get_value()) < 0) // GOOD + return value; + return 0; +} + +int explicit_compare_then_assign() { + int value; + if ((value = (get_value() < 0))) // GOOD + return value; + return 0; +} + +int parenthesized_simple_assignment() { + int value; + if ((value = get_value())) // GOOD + return value; + return 0; +} + +int assignment_outside_condition() { + int value; + value = get_value() < 0; // GOOD: The assignment is not part of a condition. + return value; +} + +int plain_comparison() { + int value = get_value(); + if (value < 0) // GOOD + return value; + return 0; +} + +int explicit_compare_then_assign_without_outer_parentheses() { + int value; + if (value = (get_value() < 0)) // GOOD + return value; + return 0; +} + +int explicit_assign_then_compare_in_for() { + int value; + for (; (value = get_other_value()) >= 0;) // GOOD + return value; + return 0; +} + +int two_explicit_assignments() { + int left, right; + if ((left = get_value()) < 0 && (right = get_other_value()) < 0) // GOOD + return left + right; + return 0; +} + +int explicit_comparison_then_compound_assign() { + int value = 0; + if ((value += (get_value() < 0))) // GOOD + return value; + return 0; +} + +int compound_assignment_without_comparison() { + int value = ~0; + if ((value &= get_value())) // GOOD + return value; + return 0; +} + +int switch_expression() { + int value; + switch (value = get_value() < 0) { // GOOD: This query only covers branching conditions. + case 0: + return value; + default: + return 0; + } +} + +#define EXPLICIT_CHECK(VALUE) \ + if (((VALUE) = get_value()) < 0) return (VALUE) + +int explicit_macro_condition() { + int value; + EXPLICIT_CHECK(value); // GOOD + return 0; +} + +template +int never_instantiated_template(T input) { + int value; + if ((value = input < 0)) // GOOD: Uninstantiated template code is excluded. + return value; + return 0; +} + +int unevaluated_assignment() { + int value; + if (sizeof(value = get_value() < 0)) // GOOD: The assignment is unevaluated. + return value; + return 0; +} + +int constant_assignment() { + int value; + if ((value = 0)) // GOOD: The right-hand side is not a comparison. + return value; + return 0; +} + +int explicit_compound_shift_then_compare() { + int value = 1; + if ((value <<= get_other_value()) > 0) // GOOD + return value; + return 0; +} + +int boolean_result_assignment() { + bool negative; + if ((negative = get_value() < 0)) // GOOD: Assigning a comparison result to a Boolean is natural. + return negative; + return 0; +} + +int boolean_result_compound_assignment() { + bool seen = false; + if ((seen |= get_value() < 0)) // GOOD: Accumulating a comparison result in a Boolean is natural. + return seen; + return 0; +} + +int explicit_static_cast_of_comparison() { + int value; + if ((value = static_cast(get_value() < 0))) // GOOD: The cast explicitly groups the comparison. + return value; + return 0; +} + +int explicit_functional_cast_of_comparison() { + int value; + if ((value = int(get_value() < 0))) // GOOD: The cast explicitly groups the comparison. + return value; + return 0; +} + +template +int instantiated_template_body(T input) { + int value; + if ((value = input < 0)) // $ Alert // BAD + return value; + return 0; +} + +int instantiate_template() { + return instantiated_template_body(get_value()); +} + +struct Comparable { + int value; +}; + +bool operator<(Comparable left, int right) { + return left.value < right; +} + +int overloaded_comparison() { + Comparable input = {get_value()}; + int value; + if ((value = input < 0)) // $ MISSING: Alert // BAD [NOT DETECTED]: overloaded operators are outside this query's scope. + return value; + return 0; +} + +int assignment_as_call_argument() { + int value; + if (identity(value = get_value() < 0)) // $ Alert // BAD: The ambiguous syntax is still in a condition. + return value; + return 0; +} + +int discarded_assignment_in_comma_expression() { + int value; + if ((value = get_value() < 0, get_other_value())) // $ Alert // BAD: Still ambiguous syntax in a condition. + return value; + return 0; +} + +int assignment_in_lambda_body() { + int value; + if ([&]() { + value = get_value() < 0; // GOOD: The lambda body is not the surrounding condition. + return true; + }()) + return value; + return 0; +} From 3715b86fc4e7a0b42b695840bf08165024120793 Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Sun, 23 Aug 2026 14:16:10 +0530 Subject: [PATCH 2/3] Refine truth-value detection --- .../AmbiguousAssignmentOfComparison.ql | 50 +++++++---- ...8-13-ambiguous-assignment-of-comparison.md | 2 +- .../AmbiguousAssignmentOfComparison.expected | 60 ++++++++------ .../AmbiguousAssignmentOfComparison/test.c | 47 ++++++++++- .../AmbiguousAssignmentOfComparison/test.cpp | 83 +++++++++++++++++-- 5 files changed, 194 insertions(+), 48 deletions(-) diff --git a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql index be3007ffa038..c767fecac9a8 100644 --- a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql +++ b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql @@ -1,7 +1,8 @@ /** - * @name Ambiguous assignment of comparison in condition - * @description Assigning the result of an unparenthesized comparison in a condition may indicate - * that the assignment and comparison are grouped incorrectly. + * @name Ambiguous assignment of comparison used as truth value + * @description Assigning the result of an unparenthesized comparison when the assignment is used + * as a truth value may indicate that the assignment and comparison are grouped + * incorrectly. * @kind problem * @problem.severity warning * @precision high @@ -14,23 +15,36 @@ import cpp -/** Gets a condition that controls branching. */ -private Expr getACondition() { - result = any(IfStmt s).getCondition() +/** Holds if the value of `expression` is directly used as a truth value. */ +private predicate isDirectlyUsedAsTruthValue(Expr expression) { + expression.isCondition() or - result = any(Loop s).getCondition() + expression = any(UnaryLogicalOperation operation).getAnOperand() or - result = any(ConditionalExpr e).getCondition() + expression = any(BinaryLogicalOperation operation).getAnOperand() } /** - * Holds if `assignment` occurs within a condition that controls branching. - * - * This includes nested expressions, such as function arguments and either operand of a comma - * expression, because the ambiguous syntax still occurs within the condition. + * Holds if the value of `expression` is used as a truth value, possibly after contributing to a + * comma, conditional, or comparison expression. */ -private predicate occursInCondition(Assignment assignment) { - assignment.getParent*() = getACondition() +private predicate isUsedAsTruthValue(Expr expression) { + isDirectlyUsedAsTruthValue(expression) + or + exists(CommaExpr comma | + expression = comma.getRightOperand() and + isUsedAsTruthValue(comma) + ) + or + exists(ConditionalExpr conditional | + expression = [conditional.getThen(), conditional.getElse()] and + isUsedAsTruthValue(conditional) + ) + or + exists(ComparisonOperation comparison | + expression = comparison.getAnOperand() and + isUsedAsTruthValue(comparison) + ) } /** @@ -46,11 +60,13 @@ from Assignment assignment, ComparisonOperation comparison where assignment.getRValue() = comparison and not isExplicitlyGrouped(comparison) and - occursInCondition(assignment) and - // Assigning a comparison result to a Boolean is normally intentional. + isUsedAsTruthValue(assignment) and + // A Boolean lvalue makes assigning the comparison result type-appropriate and normally + // intentional. not assignment.getLValue().getUnspecifiedType() instanceof BoolType and not assignment.isUnevaluated() and not assignment.isFromUninstantiatedTemplate(_) select assignment, "The '" + assignment.getOperator() + - "' operation assigns the result of an unparenthesized comparison used in a condition." + "' operation assigns the result of an unparenthesized comparison, and its result is used as " + + "a truth value." diff --git a/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md b/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md index ae9e2c5d3823..91e83979b524 100644 --- a/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md +++ b/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md @@ -2,4 +2,4 @@ category: newQuery --- * Added a new query, `cpp/ambiguous-assignment-of-comparison`, to detect assignments of - unparenthesized comparison results in conditions. + unparenthesized comparison results when the assignment is used as a truth value. diff --git a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected index 9075768e9f98..8b905f48bba2 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected +++ b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/AmbiguousAssignmentOfComparison.expected @@ -1,24 +1,36 @@ -| test.c:6:8:6:31 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.c:13:30:13:54 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.c:20:8:20:33 | ... /= ... | The '/=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.c:27:8:27:33 | ... %= ... | The '%=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.c:34:8:34:32 | ... \|= ... | The '\|=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.c:41:8:41:33 | ... >>= ... | The '>>=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.c:51:3:51:26 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:8:8:8:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:15:11:15:34 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:22:7:22:29 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:29:11:29:40 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:36:8:36:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:43:11:43:33 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:48:8:48:33 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:55:8:55:32 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:64:13:64:35 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:70:29:70:51 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:77:8:77:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:84:8:84:38 | ... <<= ... | The '<<=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:91:9:91:31 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:101:3:101:20 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:253:8:253:24 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:280:16:280:38 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | -| test.cpp:287:8:287:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison used in a condition. | +| test.c:6:8:6:31 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:13:30:13:54 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:20:8:20:33 | ... /= ... | The '/=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:27:8:27:33 | ... %= ... | The '%=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:34:8:34:32 | ... \|= ... | The '\|=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:41:8:41:33 | ... >>= ... | The '>>=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:51:3:51:26 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:102:28:102:51 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:109:15:109:38 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:116:49:116:72 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.c:130:11:130:34 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:8:8:8:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:15:11:15:34 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:22:7:22:29 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:29:11:29:40 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:36:8:36:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:43:11:43:33 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:48:8:48:33 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:55:8:55:32 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:64:13:64:35 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:70:29:70:51 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:77:8:77:30 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:84:8:84:38 | ... <<= ... | The '<<=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:91:9:91:31 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:101:3:101:20 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:253:8:253:24 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:294:27:294:49 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:301:27:301:49 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:308:9:308:31 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:315:15:315:37 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:322:23:322:45 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:329:47:329:69 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:343:47:343:69 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:350:11:350:33 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:355:12:355:34 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | +| test.cpp:360:14:360:36 | ... = ... | The '=' operation assigns the result of an unparenthesized comparison, and its result is used as a truth value. | diff --git a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c index ee78980b3672..18e7675e40ad 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c +++ b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.c @@ -82,10 +82,55 @@ int c_boolean_result_assignment(void) { int c_switch_expression(void) { int value; - switch (value = read_value() < 0) { // GOOD: This query only covers branching conditions. + switch (value = read_value() < 0) { // GOOD: The switch operand is not used as a truth value. case 0: return value; default: return 0; } } + +int c_discarded_assignment_in_comma_expression(void) { + int value; + if ((value = read_value() < 0, read_other_value())) // GOOD: The assignment result is discarded. + return value; + return 0; +} + +int c_truth_valued_assignment_in_comma_expression(void) { + int value; + if ((read_other_value(), value = read_value() < 0)) // $ Alert // BAD + return value; + return 0; +} + +int c_truth_valued_conditional_then_arm(int flag) { + int value; + if (flag ? (value = read_value() < 0) : 0) // $ Alert // BAD + return value; + return 0; +} + +int c_nested_truth_valued_comma_expression(void) { + int value; + if ((read_other_value(), (read_other_value(), value = read_value() < 0))) // $ Alert // BAD + return value; + return 0; +} + +int c_nested_discarded_comma_expression(void) { + int value; + if (((read_other_value(), value = read_value() < 0), read_other_value())) // GOOD: Discarded. + return value; + return 0; +} + +int c_logical_value_outside_branch(void) { + int value; + return (value = read_value() < 0) && read_other_value(); // $ Alert // BAD +} + +int c_returned_assignment(void) { + int value; + return value = read_value() < 0; // GOOD: The assignment result is not used as a truth value. +} diff --git a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp index 8082c7925f92..ac20faf01958 100644 --- a/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp +++ b/cpp/ql/test/query-tests/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison/test.cpp @@ -1,7 +1,7 @@ int get_value(); int get_other_value(); void *get_pointer(); -bool identity(bool value); +bool check(int value); int direct_if() { int value; @@ -74,7 +74,7 @@ int logical_or() { int nested_comparison() { int value; - if ((value = get_value() < 0) == 1) // $ Alert // BAD + if ((value = get_value() < 0) == false) // $ Alert // BAD return value; return 0; } @@ -173,7 +173,7 @@ int compound_assignment_without_comparison() { int switch_expression() { int value; - switch (value = get_value() < 0) { // GOOD: This query only covers branching conditions. + switch (value = get_value() < 0) { // GOOD: The switch operand is not used as a truth value. case 0: return value; default: @@ -277,14 +277,87 @@ int overloaded_comparison() { int assignment_as_call_argument() { int value; - if (identity(value = get_value() < 0)) // $ Alert // BAD: The ambiguous syntax is still in a condition. + if (check(value = get_value() < 0)) // GOOD: The assignment only computes an integer argument. return value; return 0; } int discarded_assignment_in_comma_expression() { int value; - if ((value = get_value() < 0, get_other_value())) // $ Alert // BAD: Still ambiguous syntax in a condition. + if ((value = get_value() < 0, get_other_value())) // GOOD: The assignment result is discarded. + return value; + return 0; +} + +int truth_valued_assignment_in_comma_expression() { + int value; + if ((get_other_value(), value = get_value() < 0)) // $ Alert // BAD + return value; + return 0; +} + +int comma_assignment_under_comparison() { + int value; + if ((get_other_value(), value = get_value() < 0) == false) // $ Alert // BAD + return value; + return 0; +} + +int nested_truth_valued_comparisons() { + int value; + if (((value = get_value() < 0) == false) == false) // $ Alert // BAD + return value; + return 0; +} + +int truth_valued_conditional_then_arm(bool flag) { + int value; + if (flag ? (value = get_value() < 0) : false) // $ Alert // BAD + return value; + return 0; +} + +int truth_valued_conditional_else_arm(bool flag) { + int value; + if (flag ? false : (value = get_value() < 0)) // $ Alert // BAD + return value; + return 0; +} + +int nested_truth_valued_comma_expression() { + int value; + if ((get_other_value(), (get_other_value(), value = get_value() < 0))) // $ Alert // BAD + return value; + return 0; +} + +int nested_discarded_comma_expression() { + int value; + if (((get_other_value(), value = get_value() < 0), get_other_value())) // GOOD: Discarded. + return value; + return 0; +} + +int nested_comma_assignment_under_comparison() { + int value; + if ((get_other_value(), (get_other_value(), value = get_value() < 0)) == false) // $ Alert // BAD + return value; + return 0; +} + +bool logical_value_outside_branch() { + int value; + return (value = get_value() < 0) && get_other_value(); // $ Alert // BAD +} + +bool logical_not_outside_branch() { + int value; + return !(value = get_value() < 0); // $ Alert // BAD +} + +int truth_valued_assignment_inside_call_argument() { + int value; + if (check((value = get_value() < 0) && get_other_value())) // $ Alert // BAD return value; return 0; } From 4f2e8f49d7b8c49363e98966c8b6fd381a5ce8aa Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Wed, 9 Sep 2026 23:39:46 +0530 Subject: [PATCH 3/3] Apply review suggestions --- .../Likely Typos/AmbiguousAssignmentOfComparison.qhelp | 4 ++-- .../Likely Typos/AmbiguousAssignmentOfComparison.ql | 7 +++---- .../2026-08-13-ambiguous-assignment-of-comparison.md | 5 +++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp index 53c001212001..e8404940bfe9 100644 --- a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp +++ b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.qhelp @@ -11,14 +11,14 @@ first and then compare it with zero.

    -

    Use parentheses to make the intended operation order explicit. To assign first and compare the +

    Use parentheses to make the intended order of operations explicit. To assign first and compare the assigned value, parenthesize the assignment. To intentionally assign the comparison result, parenthesize the comparison. An explicit cast around the comparison also makes that order clear.

    In the first condition, status receives either zero or one instead of the value -returned by read_status. The second condition explicitly performs the assignment +returned by read_status(). The second condition explicitly performs the assignment before the comparison.

    diff --git a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql index c767fecac9a8..2bfe9f7159f5 100644 --- a/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql +++ b/cpp/ql/src/Likely Bugs/Likely Typos/AmbiguousAssignmentOfComparison.ql @@ -1,8 +1,7 @@ /** - * @name Ambiguous assignment of comparison used as truth value - * @description Assigning the result of an unparenthesized comparison when the assignment is used - * as a truth value may indicate that the assignment and comparison are grouped - * incorrectly. + * @name Ambiguous assignment of comparison result used as truth value + * @description Using an assignment of an unparenthesized comparison as + * a truth value may indicate unintended operator grouping. * @kind problem * @problem.severity warning * @precision high diff --git a/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md b/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md index 91e83979b524..bcec04cbf241 100644 --- a/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md +++ b/cpp/ql/src/change-notes/2026-08-13-ambiguous-assignment-of-comparison.md @@ -1,5 +1,6 @@ --- category: newQuery --- -* Added a new query, `cpp/ambiguous-assignment-of-comparison`, to detect assignments of - unparenthesized comparison results when the assignment is used as a truth value. +* Added a new query, `cpp/ambiguous-assignment-of-comparison`, to detect potentially + ambiguous expressions where a comparison result is assigned to a variable and the + assignment is used as a truth value.