-
Notifications
You must be signed in to change notification settings - Fork 273
More precise control-flow graph using the simplifier #2031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
… false Apply the simplifier as, e.g., the C front end may generate expressions such as 0!=0 for typing purposes.
const goto_functionst &goto_functions, | ||
const goto_programt &goto_program, | ||
I PC) | ||
{ | ||
const goto_programt::instructiont &instruction=*PC; | ||
entryt entry=entry_map[PC]; | ||
assert(this->size()>entry); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pedantry but please can this be an invariant rather than an assert?
@@ -399,12 +446,13 @@ void cfg_baset<T, P, I>::compute_edges( | |||
break; | |||
|
|||
case ASSUME: | |||
case ASSERT: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with this change because I don't think that's the right semantics for assert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general "you can't get past 'assert(false)'" seems reasonable, but I note in --cover
mode we intentionally sprinkle ASSERT(FALSE)
all over the place and then selectively lift the assertions to try to achieve further coverage. In this case stopping at the first one isn't correct -- but I don't know if cfgt
would ever be used on the GOTO program that had the coverage assertions added.
So... semantics of asserts and assumes... This hasn't been documented but ... it probably should be. IMHO: ASSUME : if the condition is true continue as if nothing has happened. If it is false then immediately halt the whole process (@cesaro @tautschnig and others who have worked on multi-threading, should this be thread or process). Any assertion failures that happened before this point are still reported. ASSERT : if the condition is true continue as if nothing has happened. If it is false then report a VC failure and continue. Any assertion failures that happened before this point are still reported. If assert terminates analysis then 1. we have to add all assertions to the path condition and 2. we cannot use multiple 'assert false' conditions to find covering tests cases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the use case? When would you want the cfg pass to do this rather than running expression simplifcation yourself prior to the CFG pass?
private: | ||
friend cfg_baset<T, P, I, simp>; | ||
|
||
grapht< cfg_base_nodet<T, I> > &container; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be intentional, but note the spacing avoiding >>
is not necessary with C++11 onwards
@@ -399,12 +446,13 @@ void cfg_baset<T, P, I>::compute_edges( | |||
break; | |||
|
|||
case ASSUME: | |||
case ASSERT: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general "you can't get past 'assert(false)'" seems reasonable, but I note in --cover
mode we intentionally sprinkle ASSERT(FALSE)
all over the place and then selectively lift the assertions to try to achieve further coverage. In this case stopping at the first one isn't correct -- but I don't know if cfgt
would ever be used on the GOTO program that had the coverage assertions added.
@tautschnig pointed out in #2125 that asserts are currently possible to pass even if they fail, as @martin-cs mentioned above, so using Thinking about this more, I think the current semantics of ASSERT are bananas. In C, in Java, in basically any other language these kinds of checks abort if they fail. In particular if I wrote
Then it would be nuts to say it's possible to violate the second assertion, as in that case the first one should have aborted already. I think the only reason why ASSERT is allowed to continue at the moment is to allow us to sprinkle ASSERT FALSE around to produce coverage goals, and to permit us to hit an ASSERT FALSE target that is dominated by other ones. I suggest one of the following:
Option 2 has the advantage of being backward compatible with current cbmc. We could then always treat ASSERT(x) like "target(x); ASSUME(x)." @martin-cs @tautschnig are either of you aware of any other use of "soft assert" / check / assert-and-continue except for coverage goals? |
NB. it's also possible the intent was to allow us to e.g. verify pointer constraints with |
@smowton Thank you for your very clear message! I do share the opinion that For the proposal to move forward: I'd vote against introducing |
Actually picking up @martin-cs' earlier comment:
Yes, I think we should - this would also avoid counterexample traces with multiple violated assertions the tail of which (the part one most likely looks at on long counterexamples) usually is incomprehensible until one notices that there already is an earlier failing assertion.
As said above: we shouldn't be doing so anyway. |
Good point re: not being able to solve multiple coverage goals in one SAT session. Suspect we'd want to benchmark with Deeptest, and if it's much worse, introduce a |
Just don't do it via instrumentation. Please. Guards have all the information that you need. There is no need to add overhead to compute reachability questions that are encoded already. |
Hmmm... I hadn't planned on causing an major discussion... perhaps we can break this down a little... Things I think we can all agree on: B. The semantics of C. The question of terminate thread or terminate whole process is out of scope of this discussion and is trolling by the chronically unhelpful. D. We need a way of expressing C and Java assertions. E. We need to be able to guard against / disregard what happens after undefined / erronious behaviour such as null pointer dereferences. F. There are two possible semantics for ASSERT: G. The current semantics are Check and deviations from this were historically bugs. H. @tautschnig and @smowton are arguing that we should switch to Do-or-Die semantics. I. Do-or-Die can be modelled in terms of Check semantics by: J. Check semantics is harder to model with Do-or-Die but something like: H. In underapproximate analysis, Do-or-die style assertions should be added to the path condition but Check style ones should not be. I guess my argument in favour of Check style semantics has a number of parts:
Specific use cases where it matters:
So... yeah, I think we should stick with Check / fix things that are Do-or-Die to be check. |
Another "soft assert" use-case. --float-overflow-check and --nan-check. Generating infinity (or, with directed rounding modes, overflowing but not generating infinities) does not (on real hardware) and should not (IMHO) terminate execution. If we are checking for NaN as well, terminating could give a false sense of security as many ways of generating NaN work by generating an infinity first. Thus Do-or-Die semantics mean that adding flags / checks may reduce the number of verification failures which ... I find a bit counter-intuitive. Another one is if we are using a processor / compiler specific model of undefined behaviour (currently not done but would be desireable for security) as without Check style asserts we can't "flag an error and continue". |
More soft asserts:
I'm beginning to think this is an issue I care about... |
The goto-trace is currently cut at the first failing assertion. That's why you have never observed that situation. |
I guess the scenario @tautschnig has seen is not a single trace, but rather a summary cbmc report saying various conditions can be violated, then after some investigation it becomes clear some are spurious because they require you to somehow fail a (source level) assert then continue to fail another one. |
@martin-cs good points all. In this case I will submit a PR to turn all Java pseudo-exceptions into assert; assume, and I suggest a similar patch would be appropriate for the C frontend (but as Martin points out, not for FP checks introduced by goto-check for example) I think it would also be good to do a big cleanup that uses "assert" to mean do-or-die and "check" to mean check-and-continue so this sort of confusion is less likely to reoccur... But I don't expect that one to actually get in :) |
@martin-cs Thank you very much for the excellent elaboration (as always!) - I'm with @smowton that there should be a PR to introduce assumes after each assert that is meant to be fatal. For C I would argue that this applies to user-defined assertions as well as anything guarding undefined behaviour. |
Just or completeness... Other "soft" asserts: Also, a general principle : adding and/or removing asserts / VCs should not alter the semantics of the program. Check doesn't, Do-or-Die does. This becomes an issue when you do multi-stage analysis, such as goto-analyze --simplify first or if you slice as you can't slice away Do-or-Die assertions. At the risk of pushing my luck; can I suggest that the patches to fix this are a bit more involved than @smowton and @tautschnig suggest:
|
Here's the Java frontend part of this: #2191 |
After a discussion on issue diffblue#2031, this was agreed to be the correct semantics for the following reasons: 1. We want to check for conditions that do not result in termination. For example, generation of Inf and NaN, reachability of locations and unsigned overflow / underflow, error locations, exceptions or the use of program approximations. 2. Allowing traces with multiple assertion failures reduces the number of solver calls and test cases generated, allowing minimisation without complex program manipulation. 3. If ASSERT alters control flow then the truth of assertions can change as more assertions are added, as the failures may get blocked. Adding more checks can result in less failures. 4. If ASSERT alters control flow, then it must be taken into account when slicing, meaning that every variable in any assert must be part of the slice. 5. ASSUME allows arbitrary blocking of execution, it is better than ASSERT is orthogonal and: ASSERT(X) ASSUME(X) is used for cases where assertion failure results in termination (such as null pointer dereference or array bounds failures).
Here's the documentation fix #2193 . @peterschrammel : Does it cut after the first assert failure or at the assertion it is supposed to be checking? |
After a discussion on issue diffblue#2031, this was agreed to be the correct semantics for the following reasons: 1. We want to check for conditions that do not result in termination. For example, generation of Inf and NaN, reachability of locations and unsigned overflow / underflow, error locations, exceptions or the use of program approximations. This is particularly important for binary analysis and security. 2. Allowing traces with multiple assertion failures reduces the number of solver calls and test cases generated, allowing minimisation without complex program manipulation. 3. If ASSERT alters control flow then the truth of assertions can change as more assertions are added, as the failures may get blocked. Adding more checks can result in less failures. 4. If ASSERT alters control flow, then it must be taken into account when slicing, meaning that every variable in any assert must be part of the slice. 5. ASSUME allows arbitrary blocking of execution, it is better than ASSERT is orthogonal and: ASSERT(X) ASSUME(X) is used for cases where assertion failure results in termination (such as null pointer dereference or array bounds failures).
After a discussion on issue diffblue#2031, this was agreed to be the correct semantics for the following reasons: 1. We want to check for conditions that do not result in termination. For example, generation of Inf and NaN, reachability of locations and unsigned overflow / underflow, error locations, exceptions or the use of program approximations. This is particularly important for binary analysis and security. 2. Allowing traces with multiple assertion failures reduces the number of solver calls and test cases generated, allowing minimisation without complex program manipulation. 3. If ASSERT alters control flow then the truth of assertions can change as more assertions are added, as the failures may get blocked. Adding more checks can result in less failures. 4. If ASSERT alters control flow, then it must be taken into account when slicing, meaning that every variable in any assert must be part of the slice. 5. ASSUME allows arbitrary blocking of execution, it is better than ASSERT is orthogonal and: ASSERT(X) ASSUME(X) is used for cases where assertion failure results in termination (such as null pointer dereference or array bounds failures).
After a discussion on issue #2031, this was agreed to be the correct semantics for the following reasons: 1. We want to check for conditions that do not result in termination. For example, generation of Inf and NaN, reachability of locations and unsigned overflow / underflow, error locations, exceptions or the use of program approximations. This is particularly important for binary analysis and security. 2. Allowing traces with multiple assertion failures reduces the number of solver calls and test cases generated, allowing minimisation without complex program manipulation. 3. If ASSERT alters control flow then the truth of assertions can change as more assertions are added, as the failures may get blocked. Adding more checks can result in less failures. 4. If ASSERT alters control flow, then it must be taken into account when slicing, meaning that every variable in any assert must be part of the slice. 5. ASSUME allows arbitrary blocking of execution, it is better than ASSERT is orthogonal and: ASSERT(X) ASSUME(X) is used for cases where assertion failure results in termination (such as null pointer dereference or array bounds failures).
Closing this PR as from the discussion it is clear that there is not a resolution on how this PR should proceed, or even if it should proceed. If you believe this is erroneous, please reopen this PR. |
No description provided.