Skip to content

Partial fix for 9049: False negative: uninitialized variable with nested ifs#8680

Open
pfultz2 wants to merge 34 commits into
cppcheck-opensource:mainfrom
pfultz2:valueflow-forward-condition-fork3
Open

Partial fix for 9049: False negative: uninitialized variable with nested ifs#8680
pfultz2 wants to merge 34 commits into
cppcheck-opensource:mainfrom
pfultz2:valueflow-forward-condition-fork3

Conversation

@pfultz2

@pfultz2 pfultz2 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

This fixes the case for this:

unsigned int g();
void f(bool a) {
    unsigned int dimensions = 0;
    bool mightBeLarger;
    if (a) {
        dimensions = g();
        if (dimensions >= 1)
            mightBeLarger = false;
    } else {
        mightBeLarger = false;
    }
    if (dimensions == 1)
        return;
    if (!mightBeLarger) {}
}

Which doesnt use a compund condition dimensions >= 1 && b) and it also requires complete variables and functions. The compund condition could be handled in the future by forking within the condition, so if(a && b) { ... } can be treated as if(a) { if(b) { ... }}.

Here is a summary of the changes:

  1. Fork-based condition handling — lib/forwardanalyzer.cpp (the largest change)
  • When a condition can't be resolved, the traversal now forks: the then-branch is walked by a separate ForwardTraversal, in analyze-only mode when the value can't actually flow into it (opaque/correlated conditions like if (f(x))), so the branch's effect is tracked but nothing is reported there.
  • Branch breaks are deferred: if the else kills the value on the main path, the then-fork can still carry it forward.
  • Escapes the traversal didn't flag (e.g. unknown noreturn calls) are detected via isEscapeScope, and exit/abort are now recognized as escape functions (lib/astutils.cpp).
  1. Program-state anchoring at block boundaries — lib/vf_analyzers.cpp + lib/analyzer.h
  • assume() now anchors the assumed state at the block's end (not the condition) when control is leaving an already-traversed branch. This keeps an assumption on a variable modified inside the block (e.g. a nested if narrowing a value computed there) from being discarded as "modified" once control leaves the block.
  • New Assume::Pending flag marks the pre-traversal assume (branch walked separately) so it doesn't record premature boundary state.
  • ProgramMemoryState::assume gained an optional origin parameter so the anchor point can be overridden.
  1. vars-aware execute — lib/programmemory.cpp / .h
  • execute/conditionIsTrue/conditionIsFalse now take the tracked values (vars). A tracked value is the authoritative current value of its expression (returned and written back into the program memory), and any cached compound that depends on a tracked value is re-evaluated instead of served stale (getTrackedValue / dependsOnTrackedValue).
  • The per-assignment substitution was removed from fillProgramMemoryFromAssignments and now lives entirely in execute so it can be used for all executions.

Comment thread lib/analyzer.h Fixed
Comment thread lib/astutils.cpp
Comment on lines +2233 to +2234
if (Token::Match(ftok, "exit|abort"))
return true;

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.

There is also terminate() but it feels like should already be handled via the noreturn handling below (i.e. library configuration).

Comment thread lib/forwardanalyzer.cpp
const bool inElse = scope->type == ScopeType::eElse;
const bool inDoWhile = scope->type == ScopeType::eDo;
const bool inLoop = contains({ScopeType::eDo, ScopeType::eFor, ScopeType::eWhile}, scope->type);
const bool hasElse = Token::simpleMatch(tok, "} else {");

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.

Since it is grouped with other similar variables this is fine but in a future we should try to reduce the scope of this (i.e. avoid computing them until they are used).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was moved here so its not calculated multiple times. Probably could use the memoize function to lazily compute it once, but I think its beyond the scope here because I need to evaluate if its slower or not.

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.

I was referring to moving them after the if (!condTok) check. But as also mentioned no need to do so.

@firewave

Copy link
Copy Markdown
Collaborator

The comments and the description appear to hint at AI assistance. Is that the case?

@chrchr-github

Copy link
Copy Markdown
Collaborator

Check time: lib/astutils.cpp: 1h 31m 59.563s vs 1m 32.907s before.

Comment thread lib/programmemory.h
std::size_t operator()(const std::tuple<const Token*, const Token*, const Token*>& t) const {
const std::hash<const Token*> h;
std::size_t seed = h(std::get<0>(t));
seed ^= h(std::get<1>(t)) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
Comment thread lib/programmemory.h Fixed
Comment thread lib/programmemory.h
const std::hash<const Token*> h;
std::size_t seed = h(std::get<0>(t));
seed ^= h(std::get<1>(t)) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
seed ^= h(std::get<2>(t)) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
Comment thread lib/settings.cpp
vfOptions.doConditionExpressionAnalysis = true;
vfOptions.maxForwardBranches = -1;
vfOptions.maxForwardConditionForkDepth = 4;
vfOptions.maxForwardConditionForks = 256;
Comment thread lib/settings.h
int maxForwardConditionForkDepth = 4;

/** @brief Maximum total condition-fork continuations in one forward traversal. */
int maxForwardConditionForks = 256;
@pfultz2

pfultz2 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Check time: lib/astutils.cpp: 1h 31m 59.563s vs 1m 32.907s before.

Ok so I have improved the performance quite a bit:

Check time: lib/checkclass.cpp: 39.816s
Check time: lib/astutils.cpp: 54.508s
Check time: lib/cppcheck.cpp: 9.707s
Check time: cli/cmdlineparser.cpp: 20m 36.243s

First, it implements a cache for findExpressionChanged so multiple forks do not need to walk the tokenlist again(many times there is no change). It also calls findExpressionChanged first and then if it finds a change it will call findExpressionChangedSkipDeadCode which is slower since it needs to evaluate the conditions(which is also cached).

There can still be an explosion of forked paths here and we are using the very slow exhaustive option in CI. I still limit it to a depth of 4 on exhaustive and a limit of 256 forks when forward. Once it reaches the limit it fallbacks to updating in the main analyzer rather than forking. For reduced it uses fork limit of 0(so its disabled) and normal uses a depth of 1(so it can still catch this test case but if the ifs are nested deeper it wont).

I still think exhaustive should probably use much higher limits here. I am thinking we should add an extended option in the future to provide more analysis than normal but not as slow as exhaustive.

@danmar @chrchr-github Any feedback?

@chrchr-github

Copy link
Copy Markdown
Collaborator

Since it's quite a large change, can you run test-my-pr.py? daca shows numerous changes already in this dev cycle.

@pfultz2

pfultz2 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Since it's quite a large change, can you run test-my-pr.py? daca shows numerous changes already in this dev cycle.

Is there instructions somewhere on how to run it? I get this error when I try to do --help:

Traceback (most recent call last):
  File "tools/test-my-pr.py", line 12, in <module>
    import natsort
ModuleNotFoundError: No module named 'natsort'

@chrchr-github

Copy link
Copy Markdown
Collaborator

Since it's quite a large change, can you run test-my-pr.py? daca shows numerous changes already in this dev cycle.

Is there instructions somewhere on how to run it? I get this error when I try to do --help:

Traceback (most recent call last):
  File "tools/test-my-pr.py", line 12, in <module>
    import natsort
ModuleNotFoundError: No module named 'natsort'

pip install natsort fixed that for me.

@pfultz2

pfultz2 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

I ran over 50 packages, these are the warning diff I saw:

https://ftp.debian.org/debian/pool/main/c/cubeb/cubeb_0.0~git20250529.78ee5f0+ds.orig.tar.xz
libraries:posix,gnu,bsd
diff:
main cubeb-0.0~git20250529.78ee5f0+ds/src/cubeb_log.cpp:52:12: warning: The address of variable 'storage' might be accessed at non-zero index. [objectIndex]
     cubeb-0.0~git20250529.78ee5f0+ds/src/cubeb_log.cpp:42:26: note: Array decayed to pointer here.
     cubeb-0.0~git20250529.78ee5f0+ds/src/cubeb_log.cpp:48:16: note: Assuming condition is false
     cubeb-0.0~git20250529.78ee5f0+ds/src/cubeb_log.cpp:52:12: note: The address of variable 'storage' might be accessed at non-zero index.
https://ftp.debian.org/debian/pool/main/l/lomiri-indicator-location/lomiri-indicator-location_26.6.19.orig.tar.bz2
libraries:posix,gnu,bsd,googletest,gtk
diff:
main lomiri-indicator-location-26.6.19/tests/gtest-dbus-fixture.h:56:55: style: Either there is a missing override/final keyword, or the parameter 'name' can be declared as pointer to const. However it seems that 'wait_for_signal__timeout' is a callback function, if 'name' is declared with const you might also need to cast function pointer(s). [constParameterCallback]
     lomiri-indicator-location-26.6.19/tests/gtest-dbus-fixture.h:115:72: note: You might need to cast the function pointer here
     lomiri-indicator-location-26.6.19/tests/gtest-dbus-fixture.h:56:55: note: Either there is a missing override/final keyword, or the parameter 'name' can be declared as pointer to const
https://ftp.debian.org/debian/pool/main/u/units/units_2.27.orig.tar.gz
libraries:posix,gnu,bsd
diff:
main parse.tab.c:1366:9: warning: The address of variable 'yyptr->yyvs_alloc' might be accessed at non-zero index. [objectIndex]
     parse.tab.c:1366:9: note: Address of variable taken here.
     parse.tab.c:1366:9: note: The address of variable 'yyptr->yyvs_alloc' might be accessed at non-zero index.
https://ftp.debian.org/debian/pool/main/libe/libemf/libemf_1.0.14.orig.tar.xz
libraries:posix,gnu,bsd
diff:
main libemf-1.0.14/libemf/libemf.cpp:538:37: style: Either there is a missing override/final keyword, or the parameter 'pen' can be declared as pointer to const [constParameterPointer]
main libemf-1.0.14/libemf/libemf.cpp:594:46: style: Either there is a missing override/final keyword, or the parameter 'ext_pen' can be declared as pointer to const [constParameterPointer]
main libemf-1.0.14/libemf/libemf.cpp:631:59: style: Either there is a missing override/final keyword, or the parameter 'brush' can be declared as pointer to const [constParameterPointer]
main libemf-1.0.14/libemf/libemf.cpp:658:64: style: Either there is a missing override/final keyword, or the parameter 'font' can be declared as pointer to const [constParameterPointer]
main libemf-1.0.14/libemf/libemf.cpp:686:49: style: Either there is a missing override/final keyword, or the parameter 'palette' can be declared as pointer to const [constParameterPointer]

The objectIndex warnings were a false positive, but the constParameterPointer seems to be correct. Although, I am not sure why it changed and it seems unrelated.

@chrchr-github

Copy link
Copy Markdown
Collaborator

Yeah, those seem like recent changes unrelated to this PR. So the script doesn't use current head as the baseline for some reason.

@pfultz2

pfultz2 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

So I ran it over 200 packages and I did see one new message:

https://ftp.debian.org/debian/pool/main/g/grub/grub_0.97.orig.tar.gz
libraries:posix,gnu,bsd
diff:
your grub-0.97/stage2/disk_io.c:596:8: warning: Uninitialized variable: *len [uninitvar]
     grub-0.97/stage2/disk_io.c:526:13: note: Calling function 'next_partition', 6th argument '&len' value is <Uninit>
     grub-0.97/stage2/disk_io.c:596:8: note: Uninitialized variable: *len

But this is a true positive.

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.

4 participants