Skip to content

Fix 15042: ValueFlow: conditionally reassigned value should not be known - #8899

Open
pfultz2 wants to merge 14 commits into
cppcheck-opensource:mainfrom
pfultz2:programmmemory-multi-values
Open

pfultz2 wants to merge 14 commits into
cppcheck-opensource:mainfrom
pfultz2:programmmemory-multi-values

Conversation

@pfultz2

@pfultz2 pfultz2 commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

This refactors ProgramMemory so it can store a list of values.

A condition such as x > 3 was recorded as the possible value 4 with a lower bound, and the executor then used it as if x were exactly 4. Nested conditions were decided wrongly, so branches were skipped and values leaked past or were lost before them. This what lead to the FP in 15042.

Now it can understand these constraints better. As new values are added, it is resolved similar to Token::addValue.

Comment thread lib/programmemory.cpp Fixed
Comment thread lib/programmemory.cpp Fixed
Comment thread lib/programmemory.cpp Fixed
Comment thread lib/programmemory.cpp Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Range propagation is currently unsound for wrapping arithmetic, casts, and compound updates, with additional precision and performance issues.

Review effort: Balanced
Findings: 1 High severity · 3 Medium severity

Open (4)
What changed in this PR

Refactors value-flow program memory to retain multiple constraints, fixing incorrect branch decisions for bounded conditions such as x > 3.

Changes:

  • Stores and merges multiple value constraints per expression.
  • Propagates ranges through arithmetic, conditions, containers, and execution.
  • Adds regression and unit coverage for range-aware analysis.
File Description
lib/​programmemory.cpp Implements multi-value storage and range execution.
lib/​programmemory.h Defines the new list-based API.
lib/​valueflow.cpp Improves bound solving and loop handling.
lib/​vfvalue.h Adds range-edge helpers.
lib/​vf_common.h Adds shared saturation detection.
lib/​vf_analyzers.cpp Adapts analyzer state to value lists.
lib/​token.cpp Exposes contradiction removal.
lib/​token.h Declares contradiction-removal API.
test/​testprogrammemory.cpp Tests constraints and range execution.
test/​testvalueflow.cpp Tests range-based value flow.
test/​testnullpointer.cpp Tests null-pointer analysis with ranges.
test/​testcondition.cpp Covers issue 15042’s false positive.
Makefile Adds the new header dependency.
oss-fuzz/​Makefile Updates fuzz-build dependencies.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/programmemory.cpp Outdated
Comment on lines +879 to +883
if (op == "+") {
result = edge + k;
} else if (op == "-") {
increasing = rangeIsLhs;
result = rangeIsLhs ? edge - k : k - edge;
Comment thread lib/programmemory.cpp
Comment thread lib/programmemory.cpp Outdated
Comment on lines +1807 to +1811
const ValueFlow::Value* sizeValue = pm->getValue(value.tokvalue->exprId());
if (sizeValue && sizeValue->isContainerSizeValue())
return *sizeValue;
if (sizeValue && sizeValue->isContainerSizeValue()) {
sizes.push_back(*sizeValue);
break;
}
Comment thread lib/programmemory.cpp Outdated
Comment on lines +1905 to +1910
// Shift every value of the variable; bounds and impossible values move along
for (ValueFlow::Value& v : lhs) {
if (expr->str() == "++")
v.intvalue++;
else
v.intvalue--;
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment thread lib/programmemory.cpp Outdated
bool increasing = true;
MathLib::bigint result = 0;
if (op == "+") {
result = edge + k;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an AI review take it with a grain of salt. Please feel free to reject it by clicking on Resolve button

edge + k (and edge - k / k - edge below) can overflow the analyzer's own MathLib::bigint. isSaturated() only rejects values that are exactly at the limit. I confirmed it with a UBSan build of programmemory.cpp from this branch:

void n(long long x) {
    int* p = 0;
    if (x > 0x7ffffffffffffff0LL) {
        if (x + 100 < 0) p = (int*)&x;
        *p = 1;
    }
}
lib/programmemory.cpp:882:16: runtime error: signed integer overflow: 100 + 9223372036854775793 cannot be represented in type 'long long int'

Maybe return unknown() on overflow here, as the * branch already does via multiplyOverflows(). ceilDiv/floorDiv in valueflow.cpp have a similar corner case, LLONG_MIN / -1, which happens when rangeEdge() adds or subtracts 1 next to the limit.

For the false-positive question, I compared this branch against its merge-base on lib/, cli/, gui/, test/cfg/, samples/ and simplecpp (--enable=style,warning,performance,portability --inconclusive). The output was identical. I also tried a set of nested-condition cases with the p = 0; if (cond) p = &x; *p = 1; pattern, including unsigned wrap-around, narrow types and %/&. I found no new false positives. The only new warnings were correct "Possible null pointer dereference" for x % 4 == 0, (x & 1) == 0 and x - y == 0 under x > 3, where the old code assumed x == 4.

@pfultz2
pfultz2 requested a review from danmar October 6, 2026 19:59
@dmcppcheck

Copy link
Copy Markdown

Test results for commit 51b980a (tools/test-my-pr.py, main compared to this PR):

Test: http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8899/
packages: 1000
Differing warnings: 241 (+85 -156)
AI review: 31 improvement, 6 neutral, 13 regression (http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8899/2026-10-05_22-41-06-51b980ae69f3-review.txt)
Timing: -2.4% (http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8899/2026-10-05_22-41-06-51b980ae69f3_timing.html)

Posted automatically by the cppcheck PR test runner. +N: warnings only with this PR, -N: warnings only with main. The AI review is written by Claude and can be wrong.

@dmcppcheck

Copy link
Copy Markdown

@claude review

Comment thread lib/programmemory.cpp
if (delta.isIntValue() && mayWrap(lhs, lhsTok, op, delta.intvalue))
return forget(lhs);
for (ValueFlow::Value& v : lhs) {
const ValueFlow::Value r = evaluate(op, v, delta);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible wrong value: x op= y when y only has constraints.

The call site says "its constraints make the result unknown", but this code doesn't check for that. It passes rhs.front() as delta, and after this PR rhs.front() is often a constraint, for example after if (y > 3) or if (y != 0).

Take x = 5; if (y != 0) x += y;. Here delta is "y == 0 is impossible". evaluate("+", 5, impossible 0) returns "5 is impossible", and the loop copies intvalue (and bound) into v. v keeps its possible/known kind, so x ends up recorded as exactly 5.

With y > 3, the range path in evaluate() returns an impossible bound. v then becomes a possible value of 8 with Bound::Upper, which getIntValue() reports as x == 8.

The old code had the same problem for impossible points, but this PR records constraints much more often. Suggested fix: forget the variable when the result's kind doesn't match the operand's, e.g. if (r.isUninitValue() || r.isImpossible() != v.isImpossible()) return forget(lhs);. Or require singleValue(rhs, false) at the call site. Please also add a test like the one above in executeCompoundAssignment.

@claude

claude Bot commented Oct 9, 2026

Copy link
Copy Markdown

Reviewed the ProgramMemory refactor so it can store several values per expression. Recording conditions as impossible ranges, instead of the bound value standing in for the variable, fixes the root cause of 15042. There is good unit coverage for merging ranges, scaling and solving them, unsigned wrap-around and values at the type limits.

One issue, commented inline: in applyToValues, x op= y applies y's first constraint as if it were y's value. With x = 5; if (y != 0) x += y;, x ends up recorded as exactly 5. The old code had the same flaw, but this PR records constraints much more often. A test for that case should be added.

Minor, no change requested: an expression marked unknown (setUnknown) now comes back from execute() as a non-empty Values holding an UNINIT value. The old code treated that as unknown, and single() drops UNINIT. Current callers seem to handle it, but returning {} there would keep the meaning of "empty = unknown" consistent.

I could not build or run the branch here, so this review is from reading the code only.

🤖 Generated with Claude Code

@pfultz2

pfultz2 commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator Author

Test results for commit 51b980a (tools/test-my-pr.py, main compared to this PR):

Test: http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8899/ packages: 1000 Differing warnings: 241 (+85 -156) AI review: 31 improvement, 6 neutral, 13 regression (http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8899/2026-10-05_22-41-06-51b980ae69f3-review.txt) Timing: -2.4% (http://ec2-16-170-140-253.eu-north-1.compute.amazonaws.com/pr-8899/2026-10-05_22-41-06-51b980ae69f3_timing.html)

Posted automatically by the cppcheck PR test runner. +N: warnings only with this PR, -N: warnings only with main. The AI review is written by Claude and can be wrong.

I fixed most of the regression. There is on case with rpp that is FN, but it requires a much larger fix, so I opened the issue 15101 for that:

https://trac.cppcheck.net/ticket/15101

@dmcppcheck dmcppcheck left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review

Two new false positives found by testing; neither is in the existing comments. No crash, hang or failing test.

  1. Unsigned c - x conditions (lib/valueflow.cpp, solveExprValue): the new invertBound() is correct for signed x and removes wrong results that main had. For an unsigned x the subtraction wraps, so 64 - used > 8 does not imply used < 56. The PR now reports Condition 'used>64' is always false and a definite Null pointer dereference, where main gave only a "Possible null pointer dereference". Main was also wrong for this pattern, in the other direction, so this changes which false positives appear rather than introducing the class.
  2. Ranges that the type pins to one value: unsigned char c; if (c > 254) { if (c == 255) d = 1; return 10 / d; } now gives Division by zero. Main did not warn, because it treated c as exactly 255. This is a narrow case.

Otherwise the PR and main gave identical output on 33 varied nested-condition cases, and the other differences I saw were removed false positives or correct new "possible" warnings. A synthetic file with many if (x == N) return; lines was about 2.7× slower with the PR, but the growth is linear.

Testing

Build: make -j8 cppcheck testrunner in pr/ and make -j8 cppcheck in base/ both succeeded.

Unit tests (PR): ./testrunner for TestProgramMemory, TestValueFlow, TestCondition, TestNullPointer and TestToken all ran with 0 failures. No Python tests in test/cli are touched by the PR.

Own tests: about 110 small functions in /work/scratch/t, run through both binaries with cppcheck -q --enable=all --inconclusive --template='{line}:{id}:{message}' and diffed. They cover nested conditions, compound assignment, ++/--, unary minus, casts, container size and empty, function calls and loops.

1. New false positive: unsigned c - x

unsigned r1(unsigned len) { if (16 - len > 8) { if (len >= 8) return 1; } return 0; }
void r2(unsigned u) { if (10 - u >= 4) { if (u == 20) {} } }
int r3(unsigned used) { int *p = 0; int x = 0; if (64 - used > 8) { if (used > 64) p = &x; return *p; } return 0; }

cppcheck -q --enable=all --inconclusive --template='{line}:{id}:{message}' g.c

main:

1:knownConditionTrueFalse:Condition 'len>=8' is always true
3:nullPointer:Possible null pointer dereference: p

PR:

1:knownConditionTrueFalse:Condition 'len>=8' is always false
2:knownConditionTrueFalse:Condition 'u==20' is always false
3:knownConditionTrueFalse:Condition 'used>64' is always false
3:nullPointer:Null pointer dereference: p

All four PR lines are wrong. For example, used = 100 gives 64 - 100 = 4294967260 > 8, so used > 64 is true there. Main's line 1 was wrong too. The same code with int or unsigned char operands is handled correctly by the PR.

2. New false positive: range that the type limits to one value

int n1(unsigned char c) { int d = 0; if (c > 254) { if (c == 255) d = 1; return 10 / d; } return 0; }

cppcheck -q f.cpp

PR only (main is silent):

f.cpp:3:84: warning: Division by zero. [zerodivcond]

The lower end is fine: unsigned u; if (u < 1) { if (u == 0) ... } gives "always true" on both.

Other observations

  • Differences that are improvements or correct: a main false positive is gone for if (x >= 0) { if (x != 0) d = 1; } if (x > 0) return 10 / d;. New warnings for x <= 3 then x > 2, and for 2 <= x <= 3 then x == 3, are true positives.

  • No difference on a 33-function file (containers, compound assignments, inter-procedural calls, loops), nor on lib/programmemory.cpp, lib/checkother.cpp, lib/astutils.cpp, externals/tinyxml2/tinyxml2.cpp and test/cfg/{std.c,std.cpp,posix.c,gnu.c} (3830 identical output lines).

  • Performance: a generated function with N/2 repetitions of if (x == i) return; if (y > i) { if (x < i+1000) p = &v; } took the following times.

    N main PR
    400 1.1 s 2.9 s
    800 2.1 s 5.8 s
    1600 4.2 s 11.4 s

    That is about 2.7× slower but linear. A file with 60 nested x != k conditions was faster with the PR (3.6 s against 4.5 s).

Not tested: the GUI, and a UBSan build.


Automated review of 0f05eab by Claude. The pull request was built and tested locally.

Comment thread lib/valueflow.cpp
if (rhs) {
value.intvalue = intval - value.intvalue;
else
// c - x >= a <=> x <= c - a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review

Inverting the bound is right for signed x, but c - x wraps when x is unsigned, so c - x > a does not imply x < c - a. This now gives known results that are wrong:

int r3(unsigned used) { int *p = 0; int x = 0; if (64 - used > 8) { if (used > 64) p = &x; return *p; } return 0; }
PR:   Condition 'used>64' is always false
      Null pointer dereference: p
main: Possible null pointer dereference: p

used = 100 gives 64 - 100 = 4294967260 > 8, so used > 64 is reachable. Likewise if (10 - u >= 4) { if (u == 20) {} } now reports u==20 as always false, where main was silent.

Main was also wrong for unsigned operands here, in the other direction: for if (16 - len > 8) { if (len >= 8) ... } it said "always true" and the PR says "always false". So this is not a new class of problem, but the PR turns a "possible" null pointer warning into a definite one.

Suggest not solving a bounded value through c - x when the expression is unsigned, as mayWrap() does in the executor. Only an impossible point is safe there. A test with unsigned operands would help.

Comment thread lib/programmemory.cpp
const ValueFlow::Value& v = then ? truevalue : falsevalue;
pm.setValue(vartok, impossible ? asImpossible(v) : v);
ValueFlow::Value& v = then ? truevalue : falsevalue;
// A value with a bound is a range: record it as the range of impossible values so that it

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review

Recording x > N as a pure constraint loses the cases where the type leaves only one value. Main got these right by accident:

int n1(unsigned char c) { int d = 0; if (c > 254) { if (c == 255) d = 1; return 10 / d; } return 0; }
PR:   f.cpp:3:84: warning: Division by zero. [zerodivcond]
main: (nothing)

The lower end works (unsigned u; if (u < 1) { if (u == 0) ... is "always true"), presumably because the token already has the impossible value -1. The upper limit of the type is not combined with the recorded lower bound. This is a narrow case, but it is a new false positive. It could be fixed by collapsing the constraint to a value when getMinMaxValues() shows that the range has one element.

@danmar

danmar commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

There is on case with rpp that is FN, but it requires a much larger fix, so I opened the issue 15101 for that:

sounds great to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants