Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions clang/lib/StaticAnalyzer/Core/ExprEngineObjC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,26 @@ void ExprEngine::VisitObjCForCollectionStmt(const ObjCForCollectionStmt *S,

bool isContainerNull = state->isNull(collectionV).isConstrainedTrue();

ExplodedNodeSet dstLocation;
evalLocation(dstLocation, S, elem, Pred, state, elementV, false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To elaborate the issue, suppose evalLocation alters the Pred state and populates the new states in dstLocation. (This name dstLocation is a bit confusing IMO, let's call those new states NewPreds instead.) However, the StmtNodeBuilder below still takes Pred as the source state (old line 131) while the next transitions computed in populateObjCForDestinationSet are computed from NewPreds (old line 134 & 138).

The confusion here is that there suppose to be transitions:
Pred --evalLocation--> NewPreds --populateObjCForDestinationSet--> Dest
but the StmtNodeBuilder used for the second transition above assumes the source state is Pred instead of NewPreds. This is what's wrong with this function VisitObjCForCollectionStmt.

Then how does the bug lead to a crash?

The way StmtNodeBuilder works is that it maintains a Frontier set---the set of states to be explored next. After initialization (old line 131), Frontier = {Pred}. In populateObjCForDestinationSet, StmtNodeBuilder is used to generate Dest and remove source state from the set. Note that NewPreds are passed to populateObjCForDestinationSet as source states. So StmtNodeBuilder attempts to remove NewPreds from the set, which does not change the set. Finally, Frontier = {Pred, Dest}.

Starting from this ill-formed Frontier set, the engine executes hasMoreIterations on them. The assertion fails on the state Pred because it has never been set the ObjCForHasMoreIterations map. Dest has the map set. This is what populateObjCForDestinationSet does. (If assertions are disabled, there is a segmentation fault.)

Copy link
Contributor

Choose a reason for hiding this comment

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

(This name dstLocation is a bit confusing IMO, let's call those new states NewPreds instead.)

I would suggest keeping dstLocation (or perhaps changing it to DstLocation to align with the global coding style instead of the local tradition), because it specifies that these are the nodes produced by the "Location" step which is is more informative than NewPreds.

In general, these exploded graph node manipulating functions have a nice pattern for naming the node sets:

void ExprEngine::doSomething(ExplodedNodeSet &Src, ExplodedNodeSet &Dst, ...) {
  ExplodedNodeSet DstFoo;
  performFoo(Src, DstFoo, ...);
  //...
  ExplodedNodeSet DstBar;
  performBar(DstFoo, DstBar, ...);
  //...
  performBaz(DstBar, Dst, ...);
}

Functions like this take nodes from the node set Src (or instead start from a single node, which is usually called Pred) and place the resulting nodes in the outparameter Dst. If an operation is implemented as the combination of several lower-level steps (Foo, Bar and Baz in the example), then it's natural to introduce e.g. DstFoo as the set which will act as the Dst parameter of performFoo (and later as the Src parameter of performBar).

If there are multiple intermediate node sets, then it's important to give them descriptive names (and not just Pred -> NewPred -> NewNewPred etc.) and as the intermediate set first appears as a destination, it's easier to name it as the "destination of" a certain step. In this function NewPreds could be acceptable (as it is the only intermediate node set), but I would still prefer using the more informative name.


By the way, thanks for the detailed explanation of the issue! Your conclusions and significant changes seem to be correct, so my review is limited to bikeshedding ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the explanation. Now the name dstLocation makes sense to me.

ExplodedNodeSet DstLocation; // states in `DstLocation` may differ from `Pred`
evalLocation(DstLocation, S, elem, Pred, state, elementV, false);

ExplodedNodeSet Tmp;
StmtNodeBuilder Bldr(Pred, Tmp, *currBldrCtx);
for (ExplodedNode *dstLocation : DstLocation) {
ExplodedNodeSet DstLocationSingleton{dstLocation}, Tmp;
StmtNodeBuilder Bldr(dstLocation, Tmp, *currBldrCtx);

if (!isContainerNull)
populateObjCForDestinationSet(dstLocation, svalBuilder, S, elem, elementV,
SymMgr, currBldrCtx, Bldr,
/*hasElements=*/true);
if (!isContainerNull)
populateObjCForDestinationSet(DstLocationSingleton, svalBuilder, S, elem,
elementV, SymMgr, currBldrCtx, Bldr,
/*hasElements=*/true);

populateObjCForDestinationSet(dstLocation, svalBuilder, S, elem, elementV,
SymMgr, currBldrCtx, Bldr,
/*hasElements=*/false);
populateObjCForDestinationSet(DstLocationSingleton, svalBuilder, S, elem,
elementV, SymMgr, currBldrCtx, Bldr,
/*hasElements=*/false);

// Finally, run any custom checkers.
// FIXME: Eventually all pre- and post-checks should live in VisitStmt.
getCheckerManager().runCheckersForPostStmt(Dst, Tmp, S, *this);
// Finally, run any custom checkers.
// FIXME: Eventually all pre- and post-checks should live in VisitStmt.
getCheckerManager().runCheckersForPostStmt(Dst, Tmp, S, *this);
}
}

void ExprEngine::VisitObjCMessage(const ObjCMessageExpr *ME,
Expand Down
39 changes: 39 additions & 0 deletions clang/test/Analysis/bugfix-124477.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// RUN: %clang_analyze_cc1 -analyzer-checker=core,apiModeling,nullability.NullableDereferenced,nullability.NullabilityBase -x objective-c %s
/*
This test is reduced from a static analyzer crash. The bug causing
the crash is explained in #124477. It can only be triggered in some
rare cases so please do not modify this reproducer.
*/

#pragma clang assume_nonnull begin
# 15 "some-sys-header.h" 1 3
@class NSArray, NSObject;

@interface Base
@property (readonly, copy) NSArray *array;
@end

#pragma clang assume_nonnull end
# 8 "this-file.m" 2


@interface Test : Base

@property (readwrite, copy, nullable) NSObject *label;
@property (readwrite, strong, nullable) Test * field;

- (void)f;

@end

@implementation Test
- (void)f
{
NSObject * X;

for (NSObject *ele in self.field.array) {}
self.label = X;
}
@end


1 change: 1 addition & 0 deletions clang/unittests/StaticAnalyzer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ add_clang_unittest(StaticAnalysisTests
IsCLibraryFunctionTest.cpp
MemRegionDescriptiveNameTest.cpp
NoStateChangeFuncVisitorTest.cpp
ObjcBug-124477.cpp
ParamRegionTest.cpp
RangeSetTest.cpp
RegisterCustomCheckersTest.cpp
Expand Down
63 changes: 63 additions & 0 deletions clang/unittests/StaticAnalyzer/ObjcBug-124477.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "CheckerRegistration.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h"
#include "clang/StaticAnalyzer/Frontend/AnalysisConsumer.h"
#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h"
#include "gtest/gtest.h"

using namespace clang;
using namespace ento;

// Some dummy trait that we can mutate back and forth to force a new State.
REGISTER_TRAIT_WITH_PROGRAMSTATE(Flag, bool)

namespace {
class FlipFlagOnCheckLocation : public Checker<check::Location> {
public:
// We make sure we alter the State every time we model a checkLocation event.
void checkLocation(SVal l, bool isLoad, const Stmt *S,
CheckerContext &C) const {
ProgramStateRef State = C.getState();
State = State->set<Flag>(!State->get<Flag>());
C.addTransition(State);
}
};

void addFlagFlipperChecker(AnalysisASTConsumer &AnalysisConsumer,
AnalyzerOptions &AnOpts) {
AnOpts.CheckersAndPackages = {{"test.FlipFlagOnCheckLocation", true}};
AnalysisConsumer.AddCheckerRegistrationFn([](CheckerRegistry &Registry) {
Registry.addChecker<FlipFlagOnCheckLocation>("test.FlipFlagOnCheckLocation",
"Description", "");
});
}

TEST(ObjCTest, CheckLocationEventsShouldMaterializeInObjCForCollectionStmts) {
// Previously, the `ExprEngine::hasMoreIteration` may fired an assertion
// because we forgot to handle correctly the resulting nodes of the
// check::Location callback for the ObjCForCollectionStmts.
// This caused inconsistencies in the graph and triggering the assertion.
// See #124477 for more details.
std::string Diags;
EXPECT_TRUE(runCheckerOnCodeWithArgs<addFlagFlipperChecker>(
R"(
@class NSArray, NSDictionary, NSString;
extern void NSLog(NSString *format, ...) __attribute__((format(__NSString__, 1, 2)));
void entrypoint(NSArray *bowl) {
for (NSString *fruit in bowl) { // no-crash
NSLog(@"Fruit: %@", fruit);
}
})",
{"-x", "objective-c"}, Diags));
}

} // namespace