
Rust: Beyond the Typechecker - UkiahSmith
https://blog.merigoux.ovh/en/2019/04/16/verifying-rust.html
======
Animats
This has been done so many times.

The Stanford Pascal Verifier was probably the first[1] The Pascal-F Verifier,
a project I headed around 1980,[2][3] used preconditions and postconditions
very similar to what this guy is doing in Rust. "Specifically, full functional
correctness with properties like arithmetic correctness, in-bounds array
accesses, state machines correctness, functional correctness with respect to
an abstract specification or security properties." \- we had all that in 1983.
Look at the example starting at page 56 in [2].

DEC did a lot of work on this for Modula, and later Java, in the late
1980s.[4] Modula went down with DEC. The work was mostly lost. Dafny is
probably the most modern system in this line.

Memory safety by proof was coming along well in the early 1980s. Working
systems existed. Automatic prover technology was working. Then came C.

The "array is a pointer" mindset of C set programming back by _decades_. In
Pascal, _there were no buffer overflows_. Pascal, Modula, Ada, and on to Rust
- no buffer overflows. Everything carried along size information. Subscript
checking optimizations had been figured out by the 1980s, so the performance
penalty was coming down.

The author mentions Sapiens, an attempt to apply machine learning to the
problem of null pointers in C. (Anyone have a reference? It's hard to find.)
Massive amounts of effort are being applied to try to infer information that C
just can't express. This would be a lot more worthwhile if it resulted in
translation of C to something better.

Forty years, and this _still_ isn't fixed.

[1] [http://i.stanford.edu/pub/cstr/reports/cs/tr/79/731/CS-
TR-79...](http://i.stanford.edu/pub/cstr/reports/cs/tr/79/731/CS-
TR-79-731.pdf) [2]
[http://www.animats.com/papers/verifier/verifiermanual.pdf](http://www.animats.com/papers/verifier/verifiermanual.pdf)
[3] [https://github.com/John-Nagle/pasv](https://github.com/John-Nagle/pasv)
[4]
[https://link.springer.com/content/pdf/10.1007/BFb0026441.pdf](https://link.springer.com/content/pdf/10.1007/BFb0026441.pdf)

~~~
skywhopper
Formal methods and "proving" "correctness" have not taken off because while
they seem like a great idea, it's insanely expensive to do it for non-trivial
software, and you end up with a provably correct piece of software that is
still full of business logic bugs and which inevitably fails to keep up with
ever-changing business processes.

I agree that software is terrible, and programming language choice plays a big
role in what sorts of mistakes you can make, but I'm skeptical that attempting
to write provably correct programs is a remotely realistic goal. Sure, it's
possible to prove an algorithm, but the real world software systems that need
the help--certainly anything that involves more than one process on a single
CPU--have complexity that goes well beyond what can be modeled or reasoned
about, much less be proven.

~~~
Animats
It's not that hard to automatically prove that a large piece of software has
no buffer overflows and no null pointer dereferences. We had that working
decades ago.

Unless the code is in C. Then it's really hard.

~~~
OnlyOneCannolo
Difficult, yes, but solved [1]. There exist sound static analyzers for C and
C++. "Sound" meaning no false-negatives.

[1] [https://github.com/NASA-SW-
VnV/ikos/blob/master/analyzer/REA...](https://github.com/NASA-SW-
VnV/ikos/blob/master/analyzer/README.md#checks)

~~~
jakevn
If this has solved the issue of automatically proving that any C or C++
program has no buffer overflows, why do we still see CVEs for popular
libraries and software that result from them?

~~~
nickpsecurity
Neither FOSS projects nor commercial software use proven methods most of the
time. That's why you see the results you see. I described that here:

[http://www.ganssle.com/tem/tem372.html#article4](http://www.ganssle.com/tem/tem372.html#article4)

The tools can also miss things. They miss more as complexity goes up. High-
assurance systems used to structure things in a hierarchical way with simple
functions and only call downs to aid the analysis. Basically, reduce
combinatorial explosion. Most software isn't structured anything like that. It
does combinatorial explosion with C not giving analyzer a lot of information
to begin with. So, it causes tools to miss things.

Rust might be easier to analyze due to the type system. Those labels become
inputs and heuristics for future static analyzers.

------
ChrisSD
Here's the Servo pull request that came from formal verification of Rust code:

[https://github.com/servo/servo/pull/22458](https://github.com/servo/servo/pull/22458)

~~~
gpm
And it looks like the annotated rust code (with commented out invariants) and
f* code for that example can be found here:

[https://github.com/denismerigoux/rox-
star/blob/master/textin...](https://github.com/denismerigoux/rox-
star/blob/master/textinput/)

------
scriptkiddy
The example given with the `Point` struct is interesting. As a self taught
developer with admittedly little language design knowledge, it almost seems
like unit tests without all of the boiler plate.

For instance, the `is_diagonal` function provided as the "correctness checker"
looks to me like something that would be in a unit test for the `double`
function. However, I think it goes a little bit deeper than that. In a unit
test, you are somewhat required to pass specific values to the constructs you
are testing. The given example is more complete than a unit test in that it
doesn't just test for the correct value being produced by the `double`
function when given specific values, it actually checks all possible outcomes
by asserting that the `double` function always produces a `Point` where `p.x
== p.y`.

Given that the example is relatively trivial, I would like to see how this
would work for more complex functions and structures.

~~~
max76
The rust compiler will help you avoid a lot of common runtime errors. It comes
at a cost of extra development time. The Rust philosophy values code
correctness over developer efficiency.

~~~
im_down_w_otp
I'm confused by this sentiment. How efficient is a developer when producing
things that are incorrect? Unless the goal is produce incorrect things, which
seems unlikely.

It seems like the philosophy prefers knowing ahead of time what's wrong over
discovering it incidentally, and probably accidentally, after the fact.

~~~
coldtea
> _I 'm confused by this sentiment. How efficient is a developer when
> producing things that are incorrect?_

How is this confusing?

If it takes you 2 weeks with language X to write a sloppily written app, that
works 99% of the time but has some errors -- and 2 months with Rust to write a
version of the same app that has no errors, what you'd you prefer?

Ever heard of "time to market"? What good it would be if you get it 100%
right, and your competitor has their app already out, and captured all your
potential customers?

And even if time to market is not involved, would you really care for some
occasional errors, if you get the majority of the functionality you want
working just fine?

If you build pacemakers and airplanes, the answer is yes.

If you build regular websites, apps, and scripts, the answer might very well
be no.

That's the case, just the numbers (2 weeks, 100% etc) are made up.

~~~
camgunz
Yeah you're right, and it's almost certainly the chief constraint most
software engineers labor under.

But this is a classic race to the bottom, and regulation is coming. The days
when you could write an app that takes a person's information and stores it
negligently are drawing to a close. Given sufficient liability penalties, the
economic question will shift away from "what if we're late" to "what if it's
broken", and that's when you'll start seeing a shift away from languages like
C/C++ (and probably also languages like Python and JavaScript) towards
languages/platforms/tooling that provide strong, indemnifying guarantees.

~~~
coldtea
That would be also a great way for the large companies like Google (who can
afford to take their time and pay fines when they need) to eliminate all
smaller shops -- by extending those penalties in many places where it's not
needed (eg. no personal data involved).

Like BS regulations in cheese, wine making etc, has eliminated small regional
producers (once dominant) in favor of large conglomerates -- at least France
is somewhat holding up.

~~~
camgunz
Yeah it's kind of a quandary with regulations: larger organizations have the
resources to comply with them while smaller ones don't. There are things you
can do to work around it though (though admittedly they're rarely employed):

\- Exempt smaller organizations. Maybe the classic example is the ACA in the
US; if you're under a certain number of employees you don't need to provide
health care. Obvious pitfalls, but still an option.

\- Provide subsidies. This isn't exactly going to let a 10 person shop compete
with someone like Airbus, but it does let you do things like hire staff and
purchase tools to comply with regulations.

And separate from all that, it would be cool if we started enforcing antitrust
legislation in the US again.

------
lapinot
I'm surprised there is no name dropping of `Hoare logic' here or in the
article so i'm gonna do it. Hoare logic is made of triplets (precondition,
command, postcondition). A whole bunch of things are based on that. To go
further one can also search about separation logic which adds an account for
abstract memory locations into hoare logic.

But actually i must say i'm not very fond of these kind of approaches. As is
rightly made explicit in the post, a lot of nice properties of Rust come from
it's type system. A type system is a way to annotate usual programming
constructs with additional infos (types) together with a decidable procedure
for testing whether or not a particular program is well-typed. And then we
provide several key lemmas asserting that all well-typed program enjoy such
and such property. On the other hand, program verification is a different
beast: you annotate parts of your program with some form of specification
(hoare triplets and the like) and then you try to solve the (in most case
undecidable) problem of coming up with proofs that your program satisfies the
specifications you gave.

The approach is very different: on one hand you restrict (maybe severely) the
programs so that you can decide whether or not some properties hold, on the
other hand you (mostly) don't restrict the programs and you hope to give
enough hints so that the solver doesn't get stuck. Bottom-up versus top-down.
Correct-by-construction versus maybe-provably-correct. I feel like type
checking is robust/reliable/simple whereas verification is a black-box which
isn't easy to understand and thus never feels very "principled". Sure types
can sometimes be too explicit and heavy on the user, but they are also more
predictable and consistent.

My 2cts... Anyway i'm not saying i think this project is useless or anything,
most systems actually live somewhere between bottom-up and top-down, ideas
from one side may help the other, and deductive verification may arguably be
much more bottom-up than other verification techniques (all flavors of model
checking, abstract interpretation etc).

------
Datenstrom
My biggest wish for Rust right now is dependent types, I find myself wanting
them in almost everything I write to ensure it is correct for all possible
inputs. It could also improve performance eliminating out of bounds checks in
many places.

I recall someone mentioning in a RFC that it was on a distant wishful thinking
roadmap. I hope it is.

~~~
yawaramin
Can you give an example where you would use dependent types?

~~~
Datenstrom
Critical embedded systems for avionics and robotics systems frequently deal
with values that must be within a certain range. Runtime bounds checking can
be too expensive in these environments and dependent types would allow for
provably correct bounded integers without checks. That is actually a problem I
am dealing with right now in some MIL-STD-1553 data bus code.

While I was at NASA I was advocating for using Rust instead of C/C++ and
FORTRAN and many people were interested because of the safety/performance
aspects. I think it would take some serious ground from them in that area with
dependent types also.

~~~
mikekchar
Just to elaborate a bit more, the use of optional values rather than NULL is
really a special case dependent types. It's saying that no type can contain a
non-value. For another example, imagine subtracting something from an unsigned
int. You need to ensure that the value never goes below zero If you have an
expression like 'x - 1', then the compiler needs to ensure that x is always >
0.

The nice thing about this kind of static analysis for critical systems is that
there are often extreme consequences if a runtime check fails. What do we do
for the nuclear power control system when we detect an error? Crash? Shut down
the system? But the system is not functioning properly. How do we know that we
can safely shut it down? These are pretty horrible things to consider. It's
much better to be able to say analytically that the situation can not happen.

I once worked in a high energy physics lab, though. There is always memory
corruption to contend with, so you'll never be free of runtime errors ;-)

------
pron
Code-level specifications (sometimes called contracts) that can be verified by
various means (including manual deductive proofs, and automated solvers) exist
for mainstream programming languages. The most mature ones are probably JML
for Java (verified with [https://www.openjml.org/](https://www.openjml.org/)
and various other tools) and ACSL for C (verified with
[https://frama-c.com/](https://frama-c.com/)). C# has Spec#, but I'm not sure
it's still used.

It is still unclear how best to use such code level verification, as the
effort required grows very quickly with code size, and it's unclear how much
sporadic verification of properties that are easy to prove actually improves
correctness, but this is all still ongoing research.

~~~
garethrowlands
Often it's best to just express preconditions in the normal types. For
example, instead of having a `head-of-list` function that crashes with empty
input, define its input to be of type `NonEmptyList`.

Refinement types are also worth exploring in this space, as they appear to
give a good power-to-weight. See Liquid Haskell, for example, [https://ucsd-
progsys.github.io/liquidhaskell-tutorial/03-bas...](https://ucsd-
progsys.github.io/liquidhaskell-tutorial/03-basic.html)

~~~
pron
The power-to-weight ratio of refinement types is as unclear as that of
contracts. You get more expressivity than simple types, but it's unclear
whether it's enough to make a real impact. On the other hand, we're nowhere
near being able to verify the properties that would make a real impact on code
sizes that truly matter with automated proof techniques. Types are a bad fit
for "deep" properties to begin with because they're tied to the proof theory
of the type system. At least contracts have some leeway, as they can be
verified more weakly but with much more scalable techniques (like randomized
tests).

------
mavelikara
I found this case study by the author, linked from the article, more
interesting:
[https://blog.merigoux.ovh/en/2019/04/16/textinput.html](https://blog.merigoux.ovh/en/2019/04/16/textinput.html)

------
int_19h
Looking at examples of syntax for pre/post-conditions and invariants, it's all
Rust expressions inside strings. Is there no other way for an attribute to
have some associated inline code? Strings are not optimal for this, because
they break tooling (syntax highlighting, code completion and navigation,
refactoring etc).

~~~
steveklabnik
Until the release Thursday, it was not. As of the last release, it now is.
This was probably written before that was available generally.

~~~
int_19h
Thanks! For the curious, more details here:

[https://blog.rust-
lang.org/2019/04/11/Rust-1.34.0.html#custo...](https://blog.rust-
lang.org/2019/04/11/Rust-1.34.0.html#custom-attributes-accept-arbitrary-token-
streams)

From tooling perspective, this isn't perfect, because there's still no
guarantee that the token stream is a valid expression. So e.g. an IDE trying
to highlight them or do a global rename refactor has to assume, which might
not always be correct for token-based DSLs. It would be nice to have some
metadata to distinguish those. Or perhaps there is an idiomatic way to express
it in a contract on the macro itself, that tools could look at?

~~~
steveklabnik
Not sure, to be honest.

------
kaiby
There's actually already a library that does what the author is talking about:
[https://github.com/nrc/libhoare](https://github.com/nrc/libhoare)

They call it "design by contract" rather than "deductive verification" though.

Wish there wouldn't be so many terms for the same thing.

edit: There's also an existing RFC for this: [https://github.com/rust-
lang/rfcs/issues/1077](https://github.com/rust-lang/rfcs/issues/1077)

~~~
rrobukef
Deductive verification is a large extension of designing by contract: The
addition of a SMT solver to verify the contracts at compile time for instance.
On the other hand libhoare only adds the contracts as a runtime addition with
no mention of formally verifying these.

The same with the RFC, it dismissed automatic verification: "Automated proof
systems are very hard, especially when AFAIK Rust's type system doesn't even
have partial formal proofs yet."

This paper shows a similar approach to OP's but applied to Curry:
[https://arxiv.org/abs/1709.04816](https://arxiv.org/abs/1709.04816). It shows
how no formal proofs are needed. However Curry is significantly more pure.

------
ainar-g

      Why doesn’t this category of bugs exist for Rust programs? Simply
      because you cannot write a program that contains a null-pointer error.
    

Hate to be _that guy,_ but this is strictly not true. Observe:

    
    
      $ nl -ba nulltmp.rs 
           1 use std::ptr;
           2 
           3 fn main() {
           4     unsafe {
           5         let p = ptr::null::<i64>();
           6         println!("{}", *p);
           7     }
           8 }
      $ rustc nulltmp.rs 
      $ ./nulltmp 
      Segmentation fault (core dumped)
    

Raw pointers exist. Unsafe Rust exists. Any large enough Rust project will end
up with some unsafe code. That doesn't mean that the things OP is doing aren't
great—they are—but merely that, when considering tools to be used for real-
world Rust, people shouldn't assume that their tools will only be used in a
shiny rainbow unicorn world of Safe Rust.

~~~
int_19h
The article mentions unsafe more than once, and specifically says:

"I do not intend to tackle the difficult problem of interaction between unsafe
and safe Rust code; Rustbelt is the correct way to do it."

The statement you quoted is clearly in the implied context of safe code.

