Skip to content
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

SV-COMP: unreach-call failure in reducercommutativity/max.i #844

Closed
RyanGlScott opened this issue Sep 7, 2021 · 11 comments · Fixed by #906
Closed

SV-COMP: unreach-call failure in reducercommutativity/max.i #844

RyanGlScott opened this issue Sep 7, 2021 · 11 comments · Fixed by #906
Labels
bug crux llvm SV-COMP Issues related to making crux eligible to participate in SV-COMP.

Comments

@RyanGlScott
Copy link
Contributor

crux-llvm-svcomp incorrectly concludes that reach_error is reachable in the reducercommutativity/max.i SV-COMP benchmark program:

$ cabal run exe:crux-llvm-svcomp -- --config crux-llvm/svcomp/config-files/unreach-call.config --svcomp-arch 32bit --svcomp-spec /home/rscott2/bench-defs/sv-benchmarks/c/properties/unreach-call.prp /home/rscott2/bench-defs/sv-benchmarks/c/reducercommutativity/max.i
Resolving dependencies...
Up to date
[Crux] Using pointer width: 32 for file crux-build/crux~max.bc
[Crux] Simulating function main
[Crux] Attempting to prove verification conditions.
[Crux] Counterexample found, skipping remaining goals
[Crux] Found counterexample for verification goal
[Crux]   rscott2/bench-defs/sv-benchmarks/c/reducercommutativity/max.i:12:83: error: in reach_error
[Crux]   Call to assert()
[Crux]   Details:
[Crux]     void reach_error()
[Crux] Goal status:
[Crux]   Total: 56
[Crux]   Proved: 54
[Crux]   Disproved: 1
[Crux]   Incomplete: 1
[Crux]   Unknown: 0
[Crux] Overall status: Invalid.
[SVCOMP] Verification result: FALSIFIED (unreach-call)

There are several other programs of a similar caliber that also give the same error, but max.i is probably the most self-contained example. Crux claims that N = 0x20 is a counterexample, which is untrue. I think the use of iteration-bound: 50 in unreach-call.config might be partially responsible for this, but I'm not sure.

@RyanGlScott RyanGlScott added bug llvm crux SV-COMP Issues related to making crux eligible to participate in SV-COMP. labels Sep 7, 2021
@robdockins
Copy link
Contributor

A little experimentation leads me to believe that the counterexample chosen is usually (always?) the largest power of 2 which is below the chosen iteration bound, so it does seem related.

@robdockins
Copy link
Contributor

The iteration bound and the choice of the length of the array is a red herring. Fixing the value of N to a concrete value still causes the counterexample.

I think the actual issue here is the reads from the uninitialized array x. The way we've implemented these reads is that each read of uninitialized memory creates a fresh uninterpreted value. I suspect that this benchmark only succeeds if we return the same uninterpreted value each time.

@RyanGlScott
Copy link
Contributor Author

Oh wow, you're exactly right. Here is an even smaller program which exhibits the same issue when compiled with -O0:

#include <stdlib.h>

int main() {
  int x;
  if (x && !x) {
    abort();
  }

  return 0;
}

Logically, one would expect that x and !x could never simultaneously be true. However, each load of x results in a completely different value, which allows abort() to be reachable. Urk.

How should we fix this? I suppose one option is to check for variables that are declared without being initialized during translation, and if lax-loads-and-stores is enabled, initialize them with a specific symbolic value. Actually, that won't work so well in LLVM, where the declaration of a value and its initialization can be arbitrarily far away. Perhaps we have to maintain some mapping of uninitialized variables to their symbolic contents to make sure we read from the same contents consistently? That sounds kind of tiresome, but I'm not sure how we would do better.

@robdockins
Copy link
Contributor

I think we have two options.

  1. On every alloca and malloc, write a fresh uninterpreted value of the right shape when lax-loads-and-stores is enabled, and undo the special case handing in the memory model. We don't need to check which ones will be initialized, as they will be be overwritten later (although, we might want to omit them as an optimization in cases where it is easy). This has a certain appeal, as it makes the memory model simpler.

  2. In the lax-loads-and-stores base case, keep a cache of uninterpreted values that is populated on demand. If an uninitialized value is read, generate or load an uninterpreted value large enough to cover the allocation and slice out the part that was read.

@travitch
Copy link
Contributor

travitch commented Sep 8, 2021

For (1), would we fall back to the degenerate (wrong-ish) behavior if the size was symbolic? I know that a symbolic size is probably going to be bad in general anyway

@robdockins
Copy link
Contributor

I don't think a symbolic size is a problem, as long as we're careful. We should be able to use an uninterpreted SMT array to write enough fresh bytes in that case (or, in every case?)

@RyanGlScott
Copy link
Contributor Author

We should be able to use an uninterpreted SMT array to write enough fresh bytes in that case (or, in every case?)

Indeed, I'm wondering if we shouldn't just use doArrayStore to write a fresh byte array of the appropriate size each time when invoking doAlloca or doMalloc. We could conceivably propagate the argument type in the alloca instruction to doAlloca and supply it to storeRaw, but this would involve a fair bit of code churn. Moreover, I don't think the same trick would work for doMalloc, as we don't have easy access to the argument type when calling malloc—all we have is the number of bytes to allocate.

@langston-barrett
Copy link
Contributor

Logically, one would expect that x and !x could never simultaneously be true.

I don't think it's relevant to the behavior of lax-* flags, which already lie outside of the "official" C/C++ semantics, but this expectation isn't one that's respected by (certain versions of) Clang nor rustc:

https://www.ralfj.de/blog/2019/07/14/uninit.html

https://godbolt.org/z/TWrvcq

@RyanGlScott
Copy link
Contributor Author

A very good point. I should be more precise and avoid saying "logically" but rather "what SV-COMP expects to happen". As far as I can tell, SV-COMP expects something like (1) in #844 (comment), although I can't find any official documentation stating as such. Oh well.

RyanGlScott added a commit that referenced this issue Nov 1, 2021
…loads-and-stores

Previously, `lax-loads-and-stores` would make it so that all reads from
uninitialized memory would return a new, fresh value. This isn't quite what we
want, however, as this implies that consecutive reads from the same,
uninititalized variable would return different results. Instead, we now write a
fresh value of the right shape (using `freshConstant`/`doArrayStore`) whenever
allocating something (with `alloca`, `malloc`, or otherwise) when
`lax-loads-and-stores` is enabled.

Fixes #844.
@RyanGlScott
Copy link
Contributor Author

I whipped up an implementation of (1) from #844 (comment) at #906. It makes the test case from #844 (comment) pass, and it makes reducercommutativity/max.i report UNKNOWN instead of FALSIFIED, which is just peachy. Any feedback on the design is welcome.

@robdockins
Copy link
Contributor

robdockins commented Nov 1, 2021

Thanks for those links Langston. That lead me to the following:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1793.pdf

I've only skimmed it so far, but it seems like an excellent analysis of this question from the C standardization angle. EDIT: and it touches on the bitfield questions we have been grappling with recently as well.

RyanGlScott added a commit that referenced this issue Nov 15, 2021
Previously, `laxLoadsAndStores` would make it so that all reads from
uninitialized memory would return a new, fresh value. This isn't always what
you want, however, as this implies that consecutive reads from the same,
uninititalized variable would return different results. This patch adds another
knob in the form of the `indeterminateLoadBehavior` field of `MemOptions`,
which has two settings:

* `unstable-symbolic`: The previous behavior, in which each read from
  uninitialized memory will return a fresh symbolic value each time.
* `stable-symbolic` (now the default): After memory is allocated (be it
  through `alloca`, `malloc`, `calloc`, etc.), it is written with a fresh
  symbolic value. This makes it so that reading from the same section of
  uninitialized memory multiple times will always return the same symbolic
  value.

Note that `indeterminateLoadBehavior` only takes effect when `laxLoadsAndStores`
is enabled.

Fixes #844.
RyanGlScott added a commit that referenced this issue Nov 22, 2021
Previously, `laxLoadsAndStores` would make it so that all reads from
uninitialized memory would return a new, fresh value. This isn't always what
you want, however, as this implies that consecutive reads from the same,
uninititalized variable would return different results. This patch adds another
knob in the form of the `indeterminateLoadBehavior` field of `MemOptions`,
which has two settings:

* `unstable-symbolic`: The previous behavior, in which each read from
  uninitialized memory will return a fresh symbolic value each time.
* `stable-symbolic` (now the default): After memory is allocated (be it
  through `alloca`, `malloc`, `calloc`, etc.), it is written with a fresh
  symbolic value. This makes it so that reading from the same section of
  uninitialized memory multiple times will always return the same symbolic
  value.

Note that `indeterminateLoadBehavior` only takes effect when `laxLoadsAndStores`
is enabled.

Fixes #844.
RyanGlScott added a commit that referenced this issue Nov 22, 2021
…906)

* Revert "Use readUnwrittenMemory in readMem'"

This reverts commit 819adc8.

* crucible-llvm: Control granularity of reading uninitialized memory

Previously, `laxLoadsAndStores` would make it so that all reads from
uninitialized memory would return a new, fresh value. This isn't always what
you want, however, as this implies that consecutive reads from the same,
uninititalized variable would return different results. This patch adds another
knob in the form of the `indeterminateLoadBehavior` field of `MemOptions`,
which has two settings:

* `unstable-symbolic`: The previous behavior, in which each read from
  uninitialized memory will return a fresh symbolic value each time.
* `stable-symbolic` (now the default): After memory is allocated (be it
  through `alloca`, `malloc`, `calloc`, etc.), it is written with a fresh
  symbolic value. This makes it so that reading from the same section of
  uninitialized memory multiple times will always return the same symbolic
  value.

Note that `indeterminateLoadBehavior` only takes effect when `laxLoadsAndStores`
is enabled.

Fixes #844.

* Only apply the `stable-symbolic` semantics for heap allocations in
`doMalloc`.

This avoids a bug that was causing global variables to be incorrectly
initialized. The ultimate source of that bug still needs to be fixed,
but this avoids the bug for now, and is also the right thing to do.

* Remove a fall through case in `writeMemWithAllocationCheck`
that was sometimes causing memory writes to be silently
discarded.

In particular, writing an `LLVMZero` value into an SMT array
backed location would have no effect.  Unhandled cases in this
code will now panic instead of silently giving incorrect answers.

* Fix a bug in global initialization that could incorrectly "undo" some
global variable setup if there is a chain of global variables that
refer to others via constant expressions.

* Update crucible-llvm test suite

* Include globals in T844 test cases

* Handle "extern" global variables in the lax-loads/stable-symbolic case.

This involves making a special case so that external globals recive
an initializing write with fresh sybmolic values at global population
time.

* Add some notes about deferencing pointers with lax-loads-and-stores

Co-authored-by: Rob Dockins <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug crux llvm SV-COMP Issues related to making crux eligible to participate in SV-COMP.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants