Removing "TransferFuncs" from the analyzer

Hi Ted and cfe-dev. In my free time I've been looking into the issue of "transfer functions" in the analyzer — a supposedly swappable implementation of logic for evaluating function calls and Objective-C messages, among other things. As the Checker infrastructure has grown, almost all of the once-unique transfer function logic can now be implemented using essentially-pluggable Checkers.

That wouldn't be such a problem, except that in reality the implementation of TransferFuncs in the analyzer is CFRefCount, which mixes in a whole bunch of logic for tracking retain counts and Cocoa conventions. We shouldn't need to run that with every run of the analyzer (though most of the code will early-exit when it realizes it's not relevant). Some of the code has already been extracted to a class called RetainReleaseChecker, but at this point we can do a lot more.

This e-mail is recording my thoughts on this and how far I got; it's not intended as an immediate call to action.

In fact, the behavior of a hypothetical SimpleTransferFuncs only includes this:
- Invalidating ivars of ObjC message receiver
- Invalidating ivars of 'this' for C++ method call
- Invalidating function/method args
- Invalidating globals if necessary
- Conjuring return values

And these effects only happen if no checker claims "evalCall" for a given function call.

On the other hand, a fully-fledged RetainReleaseChecker with all existing functionality isn't quite possible because of four things:

- CFBugReports use function call summaries to show the retain/release history of a leaked object. Function summaries are currently associated with ExplodedNodes in a DenseMap. In addition to being somewhat inefficient (summaries for calls that have nothing to do with ref-counting are also saved), Checkers are const, and so can't store summaries in a mutable data structure. (Marking the map 'mutable' doesn't feel like the right solution.) I don't have a solution for this.

- There's no evalObjCMessage, for the very sensible reason that overriding a method can make its behavior totally different. Nevertheless, there are a few messages we /do/ want to model in a RetainReleaseChecker: -[NSAutoreleasePool init], -[NSAutoreleasePool addObject:], -retain, -release, and -autorelease. Fortunately, pretty much everything else would fit in a PostObjCMessage implementation.

- When function/method arguments are invalidated, their reference counts persist. But any other sort of invalidation does destroy reference count records. Currently this uses a whitelist built before invalidating the various regions referenced in a call, but with no special consideration given to RetainReleaseChecker, that'd be a little strange. I can think of two ways around this: reconstruct the whitelist on every region invalidation if we're in a function call (probably not a good idea) and reconstruct the reference bindings in a post-call check (by walking backwards through ExplodedNodes to the pre-call state).

- CFRefCount currently registers a GRState::Printer for printing the retain count info in a state. Checkers currently have no way to register printers for custom state info, and that should probably be fixed anyway.

Everything else that CFRefCount currently does can be done using the current Checker architecture. (For more detailed but less structured notes on this, see attached

CFRefCount.txt (2.75 KB)

CFRefCount.1.patch (8.43 KB)

CFRefCount.2.patch (43.1 KB)

Hi Jordy,

I'm still on paternity leave, but I hope to respond to you soon with more detail. The upshot is that I have wanted to remove TransferFuncs for a long time, and I agree with you that the Checker interface has grown enough that we can do this. I think most of the logic not specific to retain release checker can just be moved into ExprEngine (i.e., ivar invalidation). Most of the rest of the retain release checker can be done as pre- and post- conditions instead of literally evaluating the function call/methods (which we can do well with the Checker interface). The only thing we cannot model now with post conditions alone are methods that return aliases (e.g., -retain) since we do not support unification of two distinct symbols yet (i.e., saying that two symbols are actually the same). That's a really big piece to add, but it is needed in several contexts.

Ted

Looking at this some more, it seems the only information the function summaries provide that aren't also available through RefVals are:

1. Treating [object dealloc] differently from [object release]. This could be replaced by examining the current ExplodedNode's Stmt every time we hit a Released RefVal, or by adding a ReleasedByDeallocation RefVal::Kind. Or we could ignore it since it is so rare in Cocoa code. (I suppose non-Cocoa code doesn't have to be ref-counted for the RetainReleaseChecker to catch alloc/dealloc balances.)

2. Noting that (NS|CF)MakeCollectable is ignored in non-GC mode. This is definitely nice QoI-wise but isn't (IMHO) a critical part of the bug report's usefulness.

3. Noting that -autorelease, -retain, and -release are ignored in GC mode. Same as above.

Noting that X is ignored is something that's a little annoying to do while trying to keep GRState explosion down. My initial thought was to add a counter to RefVal for these messages, but that would mean that something like this would create two states after the branch is done (i.e. no uniquing the states).

id object = getObject();
if (arbitraryCondition()) {
  [object retain];
  doSomethingWith(object);
  [object release];
}

Another possibility is a simple single-bit toggle; that way any ignored messages will still show up as a change in the state of the referred variable, and only branches with an odd number of ignored messages will result in distinct GRStates. This feels like more of a hack, but it costs less in memory (it could even be combined in a bitfield with the object kind).

With either way of forcing a change, we'd then examine the current ExplodedNode's statement to see what was wrong.

I'm going to start working on this soon, using the bit toggle for the ignored messages, and try to come up with orthogonal patches that chip away at TransferFuncs and CFRefCount!

Jordy

Looking at this some more, it seems the only information the function summaries provide that aren't also available through RefVals are:

Hi Jordy,

The main role of the summaries is to memoize the computation of the transfer function logic. Those should stay around primarily to make the checker faster. You are correct, however, that they are currently used for generating the diagnostics

1. Treating [object dealloc] differently from [object release]. This could be replaced by examining the current ExplodedNode's Stmt every time we hit a Released RefVal, or by adding a ReleasedByDeallocation RefVal::Kind. Or we could ignore it since it is so rare in Cocoa code. (I suppose non-Cocoa code doesn't have to be ref-counted for the RetainReleaseChecker to catch alloc/dealloc balances.)

This is a QOI thing, and I think it is worth keeping. Adding a new RefVal::Kind seems inexpensive. All of the QOI in the retain/release checker is there for a reason (because users have explicitly requested them). I see no reason to regress on functionality here, especially when we have a way to represent them in a cleaner model.

2. Noting that (NS|CF)MakeCollectable is ignored in non-GC mode. This is definitely nice QoI-wise but isn't (IMHO) a critical part of the bug report's usefulness.

3. Noting that -autorelease, -retain, and -release are ignored in GC mode. Same as above.

Noting that X is ignored is something that's a little annoying to do while trying to keep GRState explosion down. My initial thought was to add a counter to RefVal for these messages, but that would mean that something like this would create two states after the branch is done (i.e. no uniquing the states).

id object = getObject();
if (arbitraryCondition()) {
[object retain];
doSomethingWith(object);
[object release];
}

Another possibility is a simple single-bit toggle; that way any ignored messages will still show up as a change in the state of the referred variable, and only branches with an odd number of ignored messages will result in distinct GRStates. This feels like more of a hack, but it costs less in memory (it could even be combined in a bitfield with the object kind).

I have another suggestion that avoids the state explosion problem, but it requires more infrastructure: annotated GRStates.

Essentially we are trying to reconstruct information here on how a GRState was constructed at a particular program point along a specific path. The problem is that recording path-specific information in a GRState is dangerous because it can lead to state bifurcation.

Consider a new concept: AnnotatedGRState.

In pseudo-code:

class AnnotatedGRState {
  const GRState *state;
  const Annotations *annotations;

public:
  AnnotatedGRState(const GRState *st, const Annotations *annots = 0) : state(st), annotations(annots) {}
  operator const GRState*() const { return state; }
};

and the following modification to ExplodedGraph:

  ExplodedNode* getNode(const ProgramPoint& L, const GRState *State,
                        bool* IsNew = 0);

becomes

  ExplodedNode* getNode(const ProgramPoint& L, const AnnotatedGRState &State,
                        bool* IsNew = 0);

The main change is that getNode() accepts a GRState with optional annotations. Those annotations aren't stored directly in ExplodedNode (we just keep a GRState*), but in a side-table in ExplodedGraph because annotations will be so rare. We can then query the ExplodedGraph for the annotations for a given ExplodedNode.

Of course I've left the definition of "Annotations" ambiguous; the idea is to provide something that a checker can scribble on top of a state that can be preserved in the ExplodedGraph but does *not* taint the GRState with path-specific information that we wouldn't want in subsequent nodes in the ExplodedGraph.

This approach solves a variety of problems:

(1) CFRefCount doesn't need to grovel through the AST when generating diagnostics, like it does right now, to infer "what happened" in a state transition to generate the right diagnostic. It just generates an annotation up front when it does the state transition (for those cases where we need to record more information), which gets stuck on the ExplodedNode. Subsequent ExplodedNodes don't inherit the annotations; they just get the GRState.

(2) This is a general problem that all checkers need to be able to solve. Most checkers want to have rich diagnostics, but shouldn't need to go through the same kind of complexity that CFRefCount does to generate good diagnostics.

(3) Annotations have a variety of possible implications besides just better diagnostics; they are a way of tagging points along paths that are of interest to a checker.

What do you think of this approach?

With either way of forcing a change, we'd then examine the current ExplodedNode's statement to see what was wrong.

I'm going to start working on this soon, using the bit toggle for the ignored messages, and try to come up with orthogonal patches that chip away at TransferFuncs and CFRefCount!

I won't mince words here: I really am opposed to the bit toggle approach. It doesn't feel clean, and I think there overall is a much better general approach (e.g., annotated states) that will be useful for a wider range of checkers. We shouldn't go down the path of stuffing *transient* information into a GRState that has the possibility of bifurcating the exploded graph. If we find ourselves tempted to do that, it seems to me that we are lacking the needed infrastructure to do the right thing.

The main role of the summaries is to memoize the computation of the transfer function logic. Those should stay around primarily to make the checker faster. You are correct, however, that they are currently used for generating the diagnostics

Oops, right, I meant why the summaries have to be attached to ExplodedNodes -- the only time they're used once attached is for bug reports, and only in those three cases (-dealloc, MakeCollectable in non-GC, and retain/release in GC). Which, actually, is something we could change right now, by only attaching summaries if they have interesting ArgEffects. The actual RetainSummaryManager can still live on RetainReleaseChecker when everything blows over.

For AnnotatedGRState, I think if we're going to add new infrastructure this isn't a bad way to go. I would think Annotations would be a lot like a GRState's GDM, a wrapper around some kind of tag-keyed map (though it doesn't have to be immutable until it goes into an AnnotatedGRState.

How do annotations interact with ExplodedNodes being FoldingSetNodes? Are annotations merged, or do they make two different nodes unique? (If they make the nodes unique, does that mean all annotation data—which I'm currently thinking of like a GRState's GDM—has to be profileable?)

Lastly, I'm a little wary of using AnnotatedGRState to carry around a state+annotations, rather than just adding an Annotations parameter to getNode, but I guess it makes things easier for existing code. We'll probably have to look at all the callers of getNode either way if we make this change.

I guess the big question is whether or not this is actually necessary: will checkers need a way to associate information with certain nodes /besides/ RetainReleaseChecker? Probably yes, but there should probably be a guideline to avoid over-using annotations, putting off most of the work until you know there's a diagnostic.

Or not. Maybe we decide annotations are cheap, and that checkers should go ahead and use them for better diagnostics.

Jordy

Separately, there is one last thing CFRefCount does that I missed my first time around: it determines whether we're running a GC or non-GC analysis when we're in a hybrid mode. This is something that has to be dealt with at the same time as moving what's currently in evalCall and evalObjCMessage to RetainReleaseChecker.

Now, we could just move the flag to AnalysisContextManager or ExprEngine, but neither of those feel right — is this ever going to matter to anyone besides RetainReleaseChecker? (Currently it's limited to CFRefCount.cpp.)

My alternative is to bifurcate the GRState at the start of each code body. This limits the split to when RetainReleaseChecker is active. We could even go further and make it split the state on demand, with something like this:

std::pair<const GRState*, const GRState *> splitGC (const GRState *state) {
  switch (langOpts.getGCMode()) {
  case LangOptions::NonGC:
    return pair(state, NULL);

  case LangOptions::GCOnly:
    return pair(NULL, state);

  case LangOptions::HybridGC:
    // See if we've picked a mode yet. If not, the default is HybridGC.
    switch (state->get<GCEnabled>()) {
    case LangOptions::NonGC:
      return pair(state, NULL);
    case LangOptions::GCOnly:
      return pair(NULL, state);
    case LangOptions::HybridGC: {
      const GRState *withoutGC = state->set<GCEnabled>(LangOptions::NonGC);
      const GRState *withGC = state->set<GCEnabled>(LangOptions::GCOnly);
      return pair(withoutGC, withGC);
    }
    }
  }
}

// later...
llvm::tie(stateNoGC, stateGC) = splitGC(state);

That way, it'll choose whether or not to analyze a function under GC as soon as it needs to, but no sooner. (Since this is an optimization, it'd be something to add after the CFRefCount migration is done.)

The downside of bifurcating states instead of running two analyses is increased memory usage; the upside is a probable minor speed benefit from only having to do path-insensitive checks once, walk the AST once, etc. And of course, less target-specific information in the analyzer infrastructure.

We can hold off on this piece for a while longer, though. :slight_smile:

Jordy

Hi Jordy,

Sorry I took so long to reply to this. As you know I've been occupied these last few weeks. This email also touched on some really core topics in the analyzer's design, so I wanted to take the proper time to respond to it in detail.

Comments inline.

Hi Ted and cfe-dev. In my free time I've been looking into the issue of "transfer functions" in the analyzer — a supposedly swappable implementation of logic for evaluating function calls and Objective-C messages, among other things. As the Checker infrastructure has grown, almost all of the once-unique transfer function logic can now be implemented using essentially-pluggable Checkers.

That wouldn't be such a problem, except that in reality the implementation of TransferFuncs in the analyzer is CFRefCount, which mixes in a whole bunch of logic for tracking retain counts and Cocoa conventions. We shouldn't need to run that with every run of the analyzer (though most of the code will early-exit when it realizes it's not relevant). Some of the code has already been extracted to a class called RetainReleaseChecker, but at this point we can do a lot more.

A bit of history first.

GRTransferFuncs was the original "checker" interface. It was the plugin callback logic from GRExprEngine to something outside that defined the domain-specific checker logic. I knew it would one day grow into something else, but I wanted to keep the checker logic separate from the core analysis engine, and GRTransferFuncs was a nice abstraction that serviced to provide the hooks for abstracting out checker logic.

Now we have the Checker interface, would allows us to define multiple checkers that run together. The Checker interface doesn't fully replace GRTransferFuncs, but I don't think it should. I see two possible directions:

(a) Move all retain/release checker logic out of a GRTransferFuncs subclass (where it is now) into RetainReleaseChecker. The remaining logic, which really concerns core analysis logic (e.g., ivar invalidation), gets moved to GRExprEngine. We then remove GRTransferFuncs entirely.

(b) Move all retain/release checker logic out of a GRTransferFuncs subclass into RetainReleaseChecker. Instead of moving the remaining logic into GRExprEngine, however, we keep a simplified GRTransferFuncs around.

Personally, I think we should move in the direction of (a), because what will be left in the GRTransferFuncs subclass after we move all the retain/release logic out of it will be fairly fundamental stuff to the analyzer. I also think (b) adds another conceptual entity to the codebase that isn't necessarily needed.

That said, let's explore more what you propose, and go from there.

This e-mail is recording my thoughts on this and how far I got; it's not intended as an immediate call to action.

In fact, the behavior of a hypothetical SimpleTransferFuncs only includes this:
- Invalidating ivars of ObjC message receiver
- Invalidating ivars of 'this' for C++ method call
- Invalidating function/method args
- Invalidating globals if necessary
- Conjuring return values

And these effects only happen if no checker claims "evalCall" for a given function call.

Indeed, but why not just move these into GRExprEngine entirely? Why do we even need a SimpleTransferFuncs? The only reason to have a GRTransferFuncs interface is if we want the option to swap something else in its place in the future.

So why might we want to do this? Well, there is the open issue of inter-procedural analysis. One way to do that would be to swap in a different GRTransferFuncs. But I honestly don't think that's the right direction. I think inter-procedural analysis will be a core part of the analyzer engine. Yes, we will likely modularize it, perhaps making the inter-procedural analysis configurable, but GRTransferFuncs isn't that mechanism. GRTransferFuncs was meant to capture domain-specific checker reasoning, and we have something better for that now: the Checker interface.

On the other hand, a fully-fledged RetainReleaseChecker with all existing functionality isn't quite possible because of four things:

- CFBugReports use function call summaries to show the retain/release history of a leaked object. Function summaries are currently associated with ExplodedNodes in a DenseMap. In addition to being somewhat inefficient (summaries for calls that have nothing to do with ref-counting are also saved), Checkers are const, and so can't store summaries in a mutable data structure. (Marking the map 'mutable' doesn't feel like the right solution.) I don't have a solution for this.

I don't actually see an issue here, but I recognize your concerns. Let's separate your two statements.

First, the primary and original role of the function summaries is to memoize the behavior of a function/method call for quick application during evalCall. It's a computational hack. I should add that Checkers are not strictly const. They are allowed to contain data. What they shouldn't record is data specific to analyzing a given function or path. All that data should be in GRState. There is no reason, however, that Checkers cannot have any on-the-side data to cache computation that they do over and over again, particularly if it has nothing to do with a specific path.

Note that we will likely continue to add new ways for Checkers to have on-the-side *context-sensitive* data without having to include them in the Checker object itself. In the case of path-specific data, Checker state is stored in GRStates via the GDM. There is no reason we couldn't extend this concept to allowing checkers to associate data with (say) LocationContexts if we wanted them to be able to cache some data local to the analysis of a specific function or method. We haven't needed that yet, but we have all sorts of ways for Checkers to potentially store data. Data that goes into the Checker object, however, really should be something general that is useful for the entire lifetime of the checker. The function summaries used by CFRefCount fall into that category. So from the perspective of a design argument, there is no reason why these couldn't be moved to the RetainReleaseChecker.

Now what about the mapping of ExplodedNodes to function summaries? That is indeed a hack, because we don't have ways to annotate ExplodedNodes with information that checkers could use to reconstruct critical information for generating diagnostics. If we had proper annotations for ExplodedNodes, we would be able to attach this information in a structured way to nodes. That said, what would those annotations look like? All the summaries do is record what transfer function logic was used at a given point. That might not be the most efficient annotation, but that's a fairly basic one. Moreover, summaries are reused, so we don't necessarily create many of them. In the end you are just talking about a mapping from ExplodedNodes to a handful of commonly used summaries. I'm not convinced that is a serious scalability issue.

- There's no evalObjCMessage, for the very sensible reason that overriding a method can make its behavior totally different. Nevertheless, there are a few messages we /do/ want to model in a RetainReleaseChecker: -[NSAutoreleasePool init], -[NSAutoreleasePool addObject:], -retain, -release, and -autorelease. Fortunately, pretty much everything else would fit in a PostObjCMessage implementation.

I'm not convinced that these couldn't be modeled using a PostObjCMessage implementation as well. There's nothing really special about them, and really we want to move to a place where all domain-specific knowledge can be modeled in checkers.

Consider -retain. All it does is add a +1 retain count. That could easily be a post condition. The only other thing that is hard here is that -retain returns an alias to the receiver. We don't have a good way right now for a checker to articulate that except with an evalObjCMessage, but I can envision other strategies. For example, a checker should be able to say (for many reasons) that the return value aliases the receiver, and articulate that as a /constraint/ to the ConstraintManager. This requires the ability to possibly unify symbols (i.e., the symbol for the receiver's value and the symbol for the return value), but it is really powerful. I also think this generalizes well to inter-procedural analysis. For example, suppose we had *both* domain-specific knowledge from a checker about a given function as well as information garnered from analyzing the function's implementation. That means we have two sources of information about the return value of the function call. The ability to articulate domain-specific knowledge using *declarative* constraints is really the only way to naturally unify such information.

In the absence of symbol unification, however, we can probably special case the aliasing issue, at least for this specific case. For example, we can create another (perhaps transient) Checker callback that asks: does this function/method return an alias? Then ExprEngine can get in the business of doing the aliasing, and the checker just handles the truly checker-specific stuff (e.g., the reference counts).

- When function/method arguments are invalidated, their reference counts persist. But any other sort of invalidation does destroy reference count records. Currently this uses a whitelist built before invalidating the various regions referenced in a call, but with no special consideration given to RetainReleaseChecker, that'd be a little strange. I can think of two ways around this: reconstruct the whitelist on every region invalidation if we're in a function call (probably not a good idea) and reconstruct the reference bindings in a post-call check (by walking backwards through ExplodedNodes to the pre-call state).

I think reconstructing the bindings would be extremely gross. You'd basically be hacking on semantics in what's in the Store. Unless the checker is trying to simulate a function call that has "writes" in the background, we shouldn't go this way.

I have a conflicting opinion, in that I think we just don't have the right APIs right now to express a dialog between the invalidation and the checker logic. In the case of the RetainReleaseChecker, we have information when we desire not to invalidate specific symbol values, and we do that with a whitelist. But region invalidation should only happen in a few contexts. When we invalidate regions, we can make it a cooperative process between the StoreManager and the checkers. The checkers are told what regions are being invalidated, and *why*, and the checkers should have enough information to determine if that reason is sufficient to invalidate whatever checker-specific state they are tracking.

For example, suppose we have:

  id x = [[… alloc] init]
  foo(x)

In the RetainReleaseChecker, currently we whitelist the parameter value of 'x' because it is a top-level parameter value, and we know the retain/release conventions. Thus even though the transitive values reachable through 'x' are invalidated (e.g., containing ivars), we don't need to drop the refcount information on that symbol.

However, consider:

  y->x = [… alloc] init]
  bar(y)

In this case, the value in the field 'x' wouldn't be whitelisted. This is because it is a nested value, and we really have no idea what bar() will do to it.

So what's the purpose of the whitelist? It's to capture context-specific information of how values were being used in a function call when they (or their sub values) were invalidated. But this is all just context. I don't have a concrete proposal, but essentially if the region invalidation logic communicated to the checkers what was getting invalidated, and in what contexts, the checkers could make the appropriate decision on whether or not to invalidate their associated state.

Anyhow, this is just a brain dump, but I really think this just comes down to having more declarative APIs: we are invalidating a region, so tell the checkers we are invalidating it, and the checkers are given enough context to decide what they want to do with that and modify the state accordingly. The whitelist is just a hack to construct this context. While this is similar to what you propose, there is no back-patching of the reference bindings, and the checkers stay completely out of the business of manipulating the symbolic store.

- CFRefCount currently registers a GRState::Printer for printing the retain count info in a state. Checkers currently have no way to register printers for custom state info, and that should probably be fixed anyway.

Agreed.

Everything else that CFRefCount currently does can be done using the current Checker architecture. (For more detailed but less structured notes on this, see attached <CFRefCount.txt>.)

Absolutely.

I'll follow up on your notes in a separate email (possibly off the list). I don't want to derail the focus of this first high-level response before diving into more details.

As for what I /did/ accomplish, evalBind and evalAssume can be migrated to RetainReleaseChecker almost without changes: <CFRefCount.1.patch>

This looks good.

Everything /other/ than evalCall, evalObjCMessage, and evalSummary can be moved to RetainReleaseChecker, but that leaves plenty of hackery behind: <CFRefCount.2.patch>

This also looks great.

The hackery that remains looks like it falls into the talking points above: essentially what to do with GRTransferFuncs. I personally believe with the right additional APIs, we can migrate this logic to the analyzer core, and give checkers the right tools to do what they want without needing to implement these methods or have a GRTransferFuncs class at all.

I'm interested in hearing more of your opinions here.

I'm going to stop this work here...it's where it crosses the line from "simple migration" to "design decisions". Once the four issues above are resolved, it should actually be pretty straightforward to eliminate TransferFuncs and CFRefCount from the analyzer and make RetainReleaseChecker optional. Which would be great, right?

Agreed whole-heartedly. Thanks so much for spear heading this!

Oops, right, I meant why the summaries have to be attached to ExplodedNodes – the only time they’re used once attached is for bug reports, and only in those three cases (-dealloc, MakeCollectable in non-GC, and retain/release in GC). Which, actually, is something we could change right now, by only attaching summaries if they have interesting ArgEffects. The actual RetainSummaryManager can still live on RetainReleaseChecker when everything blows over.

For AnnotatedGRState, I think if we’re going to add new infrastructure this isn’t a bad way to go. I would think Annotations would be a lot like a GRState’s GDM, a wrapper around some kind of tag-keyed map (though it doesn’t have to be immutable until it goes into an AnnotatedGRState.

How do annotations interact with ExplodedNodes being FoldingSetNodes? Are annotations merged, or do they make two different nodes unique? (If they make the nodes unique, does that mean all annotation data—which I’m currently thinking of like a GRState’s GDM—has to be profile able?)

I was thinking something very simple.

First, the design is that annotations are sparse. They rarely happen, so there is no reason to put them in the ExplodedNodes themselves. I was just thinking of ExplodedGraph of having an side table from ExplodedNodes to (optional) annotations. Basically, when ExplodedGraph::getNode() is called, it is passed an AnnotatedGRState. An AnnotatedGRState is just a package for GRState and annotations (that are somehow managed). Think of AnnotatedGRState as a value object, like SVal. ExplodedGraph::getNode() then just unpacks the GRState, does what it does now, and if the AnnotatedGRState had an annotation we just plop it in the side table.

That said, you are right about ExplodedNodes being FoldingSetNodes. Annotations really have more to do with the transition between two nodes, so perhaps the annotation should be associated with the edge between the predecessor and the successor node? That way you can have multiple annotations associated with an ExplodedNode, but those annotations are contextual to the transition. If we did this, then maybe ExplodedNode:;getNode() wouldn’t reason about annotations at all. Instead, Checker::addTransition() could call generate the new node, and then tell ExplodedGraph to add an annotation between the predecessor node (which CheckerContext knows about) and the new node. The annotation would then go into a side table.

I actually really like this idea. Right now CFRefCount (and possibly other checkers) need to walk the ExplodedGraph path to infer transition information. Annotations just record that information for transition.

If we got clever with our representation, and we wanted the option to have annotations be lightweight but possibly prolific, we can possibly optimize how edge sets are represented in ExplodedNode to better store annotations. I see that as an optimization problem. I really believe that the majority of nodes, which are not checker generated, would not have annotations.

Lastly, I’m a little wary of using AnnotatedGRState to carry around a state+annotations, rather than just adding an Annotations parameter to getNode, but I guess it makes things easier for existing code. We’ll probably have to look at all the callers of getNode either way if we make this change.

Understood. My idea for AnnotatedGRState was to provide a way to accrue annotations while manipulating a GRState, but it really is just packaging. I’m fine with not introducing a class like this for starters, and just working with some basic APIs as you suggest. There is value in keeping the APIs simple, and building up what we actually need.

I guess the big question is whether or not this is actually necessary: will checkers need a way to associate information with certain nodes /besides/ RetainReleaseChecker? Probably yes, but there should probably be a guideline to avoid over-using annotations, putting off most of the work until you know there’s a diagnostic.

I think this would be useful for a variety of checkers. The retain release checker would make use of it, and so would a general malloc/free checker, or really any type state checker (including the PthreadLockChecker). Type state checking is one of the most frequent checking we can do.

Another thing we could possibly provide are “implicit annotations.” For example, suppose we provide an API to query the annotations for an <ExplodedNode, ExplodedNode> pair. Some of those could be explicitly recorded in a side table, and some of those could be lazily generated from a Checker via a callback. For example, this is essentially what CFRefCount does: it reverse engineers state transitions when generating diagnostics. For most cases it doesn’t need the function summary (which could be captured by an explicit annotation recorded in the ExplodedGraph), and for those cases the annotation could be generated lazily and on-demand. Mechanically, much of the logic in the RetainReleaseChecker for generating diagnostics would be the same, but it could be modularized based on a generic, and extensible annotation API for node transitions.

That said, you are right about ExplodedNodes being FoldingSetNodes. Annotations really have more to do with the *transition* between two nodes, so perhaps the annotation should be associated with the edge between the predecessor and the successor node? That way you can have multiple annotations associated with an ExplodedNode, but those annotations are contextual to the transition. If we did this, then maybe ExplodedNode:;getNode() wouldn't reason about annotations at all. Instead, Checker::addTransition() could call generate the new node, and then tell ExplodedGraph to add an annotation between the predecessor node (which CheckerContext knows about) and the new node. The annotation would then go into a side table.

I actually really like this idea. Right now CFRefCount (and possibly other checkers) need to walk the ExplodedGraph path to *infer* transition information. Annotations just record that information for transition.

I like this too. Most BugReporterVisitors are looking for the first state where some condition holds, but that's implemented by comparing a node and its predecessor. Having methods explicitly reasoning about at edges could make this a lot easier.

On the other hand, can two checkers generate the same edge, differing only in annotation data? Since ExplodedNodes are uniqued, and their pred/succ sets are uniqued, my guess is that this is entirely possible. That seems to make the difference between an edge and a node much less interesting.

If we got clever with our representation, and we wanted the option to have annotations be lightweight but possibly prolific, we can possibly optimize how edge sets are represented in ExplodedNode to better store annotations. I see that as an optimization problem. I really believe that the majority of nodes, which are not checker generated, would not have annotations.

The dumb way seems to be a DenseMap<pair<ExplodedNode*, ExplodedNode*>, Annotation>, where Annotation is some kind of small map data structure. Alternately, Annotation could be a simple tag-value pair—if checkers want to store more than one annotation in a single edge, they have to do it manually, under a single tag. I think the "two checkers, same edge" problem is going to rule this out though.

(This has echoes of the SummaryMap in CFRefCount now, but available to all checkers. I guess that's all right.)

I guess the big question is whether or not this is actually necessary: will checkers need a way to associate information with certain nodes /besides/ RetainReleaseChecker? Probably yes, but there should probably be a guideline to avoid over-using annotations, putting off most of the work until you know there's a diagnostic.

I think this would be useful for a variety of checkers. The retain release checker would make use of it, and so would a general malloc/free checker, or really any type state checker (including the PthreadLockChecker). Type state checking is one of the most frequent checking we can do.

I'm guessing you mean /diagnostics/ for type state checkers, because the actual type state /is/ path-sensitive non-transient information. I can't think of a time when a checker needs to look at a past node /except/ when reverse-walking the graph, and I don't know when checkers reverse-walk the graph besides emitting path diagnostics.

Oh wait, I can think of one: persisting the retain-release whitelist across evalCall. :slight_smile: But that's even more transient than usual, and shouldn't stay in the graph after evalCall finishes. So, just kidding.

Another thing we could possibly provide are "implicit annotations." For example, suppose we provide an API to query the annotations for an <ExplodedNode, ExplodedNode> pair. Some of those could be explicitly recorded in a side table, and some of those could be lazily generated from a Checker via a callback. For example, this is essentially what CFRefCount does: it reverse engineers state transitions when generating diagnostics. For most cases it doesn't need the function summary (which could be captured by an explicit annotation recorded in the ExplodedGraph), and for those cases the annotation could be generated lazily and on-demand. Mechanically, much of the logic in the RetainReleaseChecker for generating diagnostics would be the same, but it could be modularized based on a generic, and extensible annotation API for node transitions.

Would this be where you can ask a checker to get an annotation for /any/ edge? Or where a checker registers an annotation on a specific edge, but with a callback rather than an immediate value? (The former seems to have little benefit over explicitly calling a helper function.)

What do you see as a "usual" annotation? A direct translation of CFRefCount would be "any function summary", possibly optimized to "any function summary that affects ref counts". My original plan was "events that can't (easily) be inferred from a change in RefVal". It sounds like you might be suggesting "all changes in RefVals", so that we can directly ask something like

graph->getAnnotation<RefValChange>(edge, symbol);

I'll admit that looks pretty, but I'm not sure I'd want to be recording that much information in the table when you really can just compare the two nodes' states' RefVals. And if this calls a callback on RetainReleaseChecker, well, why not just do that to begin with?

this->getChange(edge, symbol);

...but mostly I'm just trying to play devil's advocate so that we don't pick something because it's the /only/ idea. (I'm glad you think adding the new infrastructure is worth not abusing the path-sensitive data; that's a call I wouldn't make.)

Jordy

One amendment: I do recognize that the checker call backs are currently marked ‘const’. I took this road to enforce a mentality that the checker writers should think about keeping state in GRState, and not in the checker itself. First-time checker writers often don’t understand why keeping information specific to a path in GRState is so critical. This decision can always be revisited.

My point, however, was that the idea of keeping data associated with a Checker is not strictly taboo if done correctly. If we want to enforce the 'const’ness strictly, then it may be worthwhile to provide checkers a way to store other side data.

In fact, the behavior of a hypothetical SimpleTransferFuncs only includes this:
- Invalidating ivars of ObjC message receiver
- Invalidating ivars of 'this' for C++ method call
- Invalidating function/method args
- Invalidating globals if necessary
- Conjuring return values

And these effects only happen if no checker claims "evalCall" for a given function call.

Indeed, but why not just move these into GRExprEngine entirely? Why do we even need a SimpleTransferFuncs? The only reason to have a GRTransferFuncs interface is if we want the option to swap something else in its place in the future.

Hm, good point. I don't think we'd be replacing the current transfer funcs anytime soon, so we probably wouldn't need to keep them around in the future.

So why might we want to do this? Well, there is the open issue of inter-procedural analysis. One way to do that would be to swap in a different GRTransferFuncs. But I honestly don't think that's the right direction. I think inter-procedural analysis will be a core part of the analyzer engine. Yes, we will likely modularize it, perhaps making the inter-procedural analysis configurable, but GRTransferFuncs isn't that mechanism. GRTransferFuncs was meant to capture domain-specific checker reasoning, and we have something better for that now: the Checker interface.

Now what about the mapping of ExplodedNodes to function summaries? That is indeed a hack, because we don't have ways to annotate ExplodedNodes with information that checkers could use to reconstruct critical information for generating diagnostics. If we had proper annotations for ExplodedNodes, we would be able to attach this information in a structured way to nodes. That said, what would those annotations look like? All the summaries do is record what transfer function logic was used at a given point. That might not be the most efficient annotation, but that's a fairly basic one. Moreover, summaries are reused, so we don't necessarily create many of them. In the end you are just talking about a mapping from ExplodedNodes to a handful of commonly used summaries. I'm not convinced that is a serious scalability issue.

I suppose it's not: in a 2000-node stress test this probably maxes out at 16~32KB to store the map; much less to store the cache of summaries. Hardly a big memory drain (numbers totally made up, though).

The discussion of ExplodedNode- or ExplodedGraph-edge-associated annotations should probably stay in the other thread fork. RetainReleaseChecker would most likely keep the summary manager code as is for its regular work; sorry to be unclear.

- There's no evalObjCMessage, for the very sensible reason that overriding a method can make its behavior totally different. Nevertheless, there are a few messages we /do/ want to model in a RetainReleaseChecker: -[NSAutoreleasePool init], -[NSAutoreleasePool addObject:], -retain, -release, and -autorelease. Fortunately, pretty much everything else would fit in a PostObjCMessage implementation.

I'm not convinced that these couldn't be modeled using a PostObjCMessage implementation as well. There's nothing really special about them, and really we want to move to a place where all domain-specific knowledge can be modeled in checkers.

Consider -retain. All it does is add a +1 retain count. That could easily be a post condition. The only other thing that is hard here is that -retain returns an alias to the receiver. We don't have a good way right now for a checker to articulate that except with an evalObjCMessage, but I can envision other strategies. For example, a checker should be able to say (for many reasons) that the return value aliases the receiver, and articulate that as a /constraint/ to the ConstraintManager. This requires the ability to possibly unify symbols (i.e., the symbol for the receiver's value and the symbol for the return value), but it is really powerful. I also think this generalizes well to inter-procedural analysis. For example, suppose we had *both* domain-specific knowledge from a checker about a given function as well as information garnered from analyzing the function's implementation. That means we have two sources of information about the return value of the function call. The ability to articulate domain-specific knowledge using *declarative* constraints is really the only way to naturally unify such information.

IPA's going to wreak havoc on our current evalCall-based checkers. :slight_smile: I agree that the 'retain' effect is easy to model post-call, but the aliasing thing is a problem. A postExpr-based checker can't even assume that a conjured return value hasn't already been squirrelled away in some other checker's slice of the GDM.

Even if we tag it with a FIXME and/or limit evalObjCMessage to RetainReleaseChecker, I think we should take the shortcut for now and save the aliasing problem for later. Though we should probably /only/ use it for receiver-aliasing -retain and -autorelease, and use postExpr<ObjCMessageExpr> for everything else (including the other effects of -retain and -autorelease).

In the absence of symbol unification, however, we can probably special case the aliasing issue, at least for this specific case. For example, we can create another (perhaps transient) Checker callback that asks: does this function/method return an alias? Then ExprEngine can get in the business of doing the aliasing, and the checker just handles the truly checker-specific stuff (e.g., the reference counts).

That seems like a very specific check that would only be useful until we got a [[return_alias]] attribute, then got it into all the frameworks. :slight_smile: But it would solve the problem.

- When function/method arguments are invalidated, their reference counts persist. But any other sort of invalidation does destroy reference count records. Currently this uses a whitelist built before invalidating the various regions referenced in a call, but with no special consideration given to RetainReleaseChecker, that'd be a little strange. I can think of two ways around this: reconstruct the whitelist on every region invalidation if we're in a function call (probably not a good idea) and reconstruct the reference bindings in a post-call check (by walking backwards through ExplodedNodes to the pre-call state).

I think reconstructing the bindings would be extremely gross. You'd basically be hacking on semantics in what's in the Store. Unless the checker is trying to simulate a function call that has "writes" in the background, we shouldn't go this way.

I have a conflicting opinion, in that I think we just don't have the right APIs right now to express a dialog between the invalidation and the checker logic. In the case of the RetainReleaseChecker, we have information when we desire not to invalidate specific symbol values, and we do that with a whitelist. But region invalidation should only happen in a few contexts. When we invalidate regions, we can make it a cooperative process between the StoreManager and the checkers. The checkers are told what regions are being invalidated, and *why*, and the checkers should have enough information to determine if that reason is sufficient to invalidate whatever checker-specific state they are tracking.

That's not a bad idea, but it's hard to do. Currently, the StoreManager has no idea why things are invalidated, either; it just has a "current expr" pointer. (It's worth noting that currently we /only/ invalidate regions due to function or methods calls, including C++ 'new' calls and constructors...and also including CStringChecker::evalCall. So maybe passing the current expr down would be "good enough".)

Hm, maybe I should stop forking the threads of conversation, and stick to one issue at a time. Though it might be worth checking in the "free" changes that can be migrated cleanly out of CFRefCount, so that all that's left is the actual problems.

Thanks for your patience and all the help with this. :slight_smile:
Jordy

Just to pull this one out with a brief comment: this isn’t specific to checkers. For example, if we see:

void foo(int *a, int *b) {

if (a == b) {

}
}

We currently don’t have a good way to reason about the case where “a == b”. If we have acquired constraints on the symbols referenced by ‘a’ and ‘b’, then they have to be unified on the true branch (and if the constraints are incompatible, the branch is infeasible). This pops up more times than one might think.

I agree that this is something to explore as a separate discussion in its own right, but it is a key piece of missing analyzer functionality.

Oh, yes, aliasing is very important. Whether or not a specific function/method call returns an alias seems less so, since eventually a postCall checker could do that naively with assumeEQ. But I suppose "eventually" might be "in a month" or it might be "years from now when someone else is looking for a GSoC project", so maybe a temporary returnsArgAlias check is better than a temporary evalObjCMessage check.