Verified Rust for low-level systems code

Related tags

Command-line verus
Overview

See Goals for a brief description of the project's goals.

Building the project

The main project source is in source.

tools contains scripts for setting up the development environment by cloning a modified version of the rust compiler into a new rust directory. Thus far, we have made only minor modifications to the Rust compiler, primarily to add additional hooks for the verification code.

See source/CODE.md for more details about files in source. See the official docs for more about the normal Rust compiler.

Step 1: Build Rust

On Linux and macOS, start in the project root directory and run the tools/set-up-rust.sh script.

On Windows you need to perform Step 1 manually:

Build the Rust compiler (manually)

Build the rust compiler from https://github.com/secure-foundations/rust with python x.py install in the rust directory:

git clone https://github.com/secure-foundations/rust.git
cd rust
cp config.toml.verify config.toml
python x.py install

(Or use python3, if that's what you've got.)

Running x.py install creates both a build and an install directory in the rust directory:

$ ls
build
install

All the installation goes into the project install directory; nothing is installed outside the project directory (see prefix = "install" in config.toml.verify for details).

Note: this first step may take more than an hour, since the Rust source code is large, but all other steps are fast.

Change directory back to the project root:

cd ..

You can pull in future updates to our fork of rust via tools/update-rust.sh.

Step 2: Setup z3

Change directory to source:

cd source

On Windows: make sure the Z3 executable is in your path

Download the Z3 binaries. Make sure you get Z3 4.8.5. The Z3 bin folder contain the executable z3.exe. Either add the Z3 bin folder to your path or copy the Z3 executable file to one of the folders in your path.

On Unix/macOS: get a local Z3

From source, use the script ./tools/get-z3.sh to download Z3. The ./tools/cargo.sh script will correctly set the VERUS_Z3_PATH environment variable for the verifier to find Z3. If you run the verifier manually, set VERUS_Z3_PATH to path_to/verify/z3.

Step 3: Build the verifier

You should be in the source subdirectory.

Set the RUSTC environment variable to point to ../rust/install/bin/rustc and use cargo to build the verifier:

For example, on macOS and Linux:

RUSTC=../rust/install/bin/rustc ../rust/install/bin/cargo build

This will build four crates:

  • three crates that constitute the verifier:
    • AIR (assertion intermediate language): an intermediate language based on assert and assume statements, which is translated into SMT queries for Z3
    • VIR (verification intermediate language): a simplified subset of Rust, which is translated into AIR
    • rust_verify, which contains a main function that runs Rust and translates the Rust intermediate representation into VIR
  • one crate that contains built-in definitions used by code being verified:
    • builtin

Running the verifier

After running the build steps above, you can verify an example file. From the source directory, run:

on Windows:

../rust/install/bin/rust_verify --pervasive-path pervasive --extern builtin=../rust/install/bin/libbuiltin.rlib --extern builtin_macros=../rust/install/bin/libbuiltin_macros.dll --edition=2018 rust_verify/example/recursion.rs

You can also use the helper script on Linux and macOS:

./tools/rust-verify.sh rust_verify/example/recursion.rs

This runs the Rust --> VIR --> AIR --> Z3 pipeline on recursion.rs and reports the errors that Z3 finds.

The -L ../rust/install/bin/ is used to link to the builtin crate.

Editing the source code

You should make sure that your check-out of rust is up to date. Use the ./tools/update-rust.sh script from the project root.

Before committing any changes to the source code, make sure that it conforms to the rustfmt tool's guidelines. We are using the default rustfmt settings from the Rust repository. To check the source code, type the following from the source directory:

../rust/install/bin/cargo-fmt -- --check

If you have other toolchains installed (with rustup) this will run the active toolchain by default, and not the rust-fmt that we compiled with the rust compiler.

To switch to the correct tools, you can add the custom toolchain to rustup, and set an override for this project:

cd ..
## In the project root:
rustup toolchain link rust-verify rust/install/
rustup override set rust-verify

If the source code follows the guidelines, cargo-fmt -- --check will produce no output. Otherwise, it will report suggestions on how to reformat the source code.

To automatically apply these suggestions to the source code, type:

../rust/install/bin/cargo-fmt

Documentation

Commenting the code is strongly encouraged. Use /// to create comments that rustdoc can automatically extract into HTML documentation.

You can compile the current documentation by running (in the verify directory)

RUSTC=../rust/install/bin/rustc RUSTDOC=../rust/install/bin/rustdoc ../rust/install/bin/cargo doc 

which will produce documentation files, e.g., ./target/doc/rust_verify/index.html

Running tests for the rust to vir translation, and inspecting the resulting vir/air/smt

cargo test will run the tests for rust_verify,

RUSTC=../rust/install/bin/rustc ../rust/install/bin/cargo test -p rust_verify

As discussed above, you may only need the RUSTC variable on macOS/Linux.

You can run a single test file and a specific test within with the following:

RUSTC=../rust/install/bin/rustc ../rust/install/bin/cargo test -p rust_verify --test <test file> <test name>

See the cargo help for more info on the test flags.

If you'd like to inspect the vir/air/smt produced by a test, you can provide a target directory path as an environment variable, VERIFY_LOG_IR_PATH. You should only run a single test, as only the latest logged IR is preserved. For example, the following will emit the vir/air/smt logs to rust_verify/logs:

VERIFY_LOG_IR_PATH="logs" RUSTC=../rust/install/bin/rustc ../rust/install/bin/cargo test -p rust_verify --test refs_basic test_struct_ref
Comments
  • `unknown` for a very simple failing assertion

    `unknown` for a very simple failing assertion

    After running Verus with the below example, Z3 gives unknown with (:reason-unknown "(incomplete quantifiers)"). I am wondering if this behavior is expected.

    fn test_n(n: nat) {
        assert(n >= 5);      // FAILS
    }
    

    I expected to see sat for this example but I was a bit surprised to see unknown. @jaybosamiya and I looked into the prelude a bit and found the prelude itself ends in unknown. There are several axioms that give unknown itself. For example, the first assert in the prelude also gives unknown.

    ;; ... 
    ;; set-option & declaration 
    ;; ...
    
    (assert
     (=>
      fuel_defaults
      (forall ((id FuelId)) (!
        (= (fuel_bool id) (fuel_bool_default id))
        :pattern ((fuel_bool id))
    ))))
    (check-sat)      ;; this gives unknown
    
    opened by channy412 15
  • VIR Printer options for concise printing (`--vir-log-option`)

    VIR Printer options for concise printing (`--vir-log-option`)

    Span often makes it much harder to read VIR logs. This pull request adds --vir-log-config command line argument to make Verus optionally include span in VIR logs.

    When vir-log-config is 0(Default), it logs all but span. when it is 1, it logs all items (as the current version does)

    opened by channy412 13
  • Implement exec mode map: dict

    Implement exec mode map: dict

    I was trying to implement an exec mode map, Dict. This PR is not done as some of the methods like remove are not implemented yet. Please let me know whether a Dict is expected to be included in pervasive and I am happy to continue working on this PR and contribute to verus.

    opened by marshtompsxd 13
  • Triggers matched on out-of-context lemmas

    Triggers matched on out-of-context lemmas

    Hello! I have a question about a potential QI (performance) issue in Verus.

    Consider this snippet of Verus code:

    spec fn abs(a: int) -> int
    {
        if a >= 0 {
            a
        } else {
            -a
        }
    }
    
    #[verifier(nonlinear)]
    proof fn equal_square_auto()
        ensures forall |a: int, b: int| a * a == b * b ==> abs(a) == abs(b)
    {}
    
    #[verifier(nonlinear)]
    proof fn equal_square_auto2()
        ensures forall |a: int, b: int| a * a == b * b ==> abs(a) == abs(b)
    {}
    
    proof fn fact(a: int)
        requires a * a == 1
        ensures a == 1 || a == -1
    {
        equal_square_auto();
        assert(abs(a) == abs(1));
    }
    

    where I have two auto lemmas equal_square_auto and equal_square_auto2 with exactly the same statement. And then in the fact proof function I’m using only the first lemma to prove something.

    However, running rust_verify with --profile-all shows that both lemmas get instantiated:

    note: Observed 20 total instantiations of user-level quantifiers
    
    note: Cost * Instantiations: 448 (Instantiated 16 times - 80% of the total, cost 28) top 1 of 2 user-level quantifiers.
      --> test.rs:20:56
       |
    20 |     ensures forall |a: int, b: int| a * a == b * b ==> abs(a) == abs(b)
       |             -------------------------------------------^^^^^^----^^^^^^
       |             |
       |             Triggers selected for this quantifier
    
    note: Cost * Instantiations: 16 (Instantiated 4 times - 20% of the total, cost 4) top 2 of 2 user-level quantifiers.
      --> test.rs:25:56
       |
    25 |     ensures forall |a: int, b: int| a * a == b * b ==> abs(a) == abs(b)
       |             -------------------------------------------^^^^^^----^^^^^^
       |             |
       |             Triggers selected for this quantifier
    

    Furthermore, when I look at the terms used for instantiation of the first lemma equal_square_auto, it actually uses the skolem constants of the second lemma equal_square_auto2 (which should be out of context).

    Then I was trying to read the SMT query for this function, and the relevant parts look like this:

    ;; Function-Axioms crate::equal_square_auto
    (declare-fun ens%equal_square_auto. () Bool)
    (assert
     (= ens%equal_square_auto. (forall ((a~10$ Poly) (b~12$ Poly)) (!
        (=>
         ...
         (=>
          (= (Mul (%I a~10$) (%I a~10$)) (Mul (%I b~12$) (%I b~12$)))
          (= (abs.? a~10$) (abs.? b~12$))
        ))
        :pattern ((abs.? a~10$) (abs.? b~12$))
        :qid user_crate__equal_square_auto_0
        :skolemid skolem_user_crate__equal_square_auto_0
    ))))
    
    ;; Function-Axioms crate::equal_square_auto2
    (declare-fun ens%equal_square_auto2. () Bool)
    (assert
     (= ens%equal_square_auto2. (forall ((a~10$ Poly) (b~12$ Poly)) (!
        (=>
         ...
         (=>
          (= (Mul (%I a~10$) (%I a~10$)) (Mul (%I b~12$) (%I b~12$)))
          (= (abs.? a~10$) (abs.? b~12$))
        ))
        :pattern ((abs.? a~10$) (abs.? b~12$))
        :qid user_crate__equal_square_auto2_1
        :skolemid skolem_user_crate__equal_square_auto2_1
    ))))
    
    
    ; the actual VC
    (assert
      (=>
       %%query%%
       (not (=>
         ens%equal_square_auto.
         ....
    

    I think what’s happening here is that Z3 somehow matched the patterns abs(...) of the first lemma on the subterms of the skolemized version of the second (unused) lemma.

    question 
    opened by zhengyao-lin 9
  • Make termination checks for exec functions, add support for mutable references in parameters, and emit a warning that the check doesn't account for loops

    Make termination checks for exec functions, add support for mutable references in parameters, and emit a warning that the check doesn't account for loops

    I do not believe this opens to any potential unsoundness, especially because it's already possible to write a non-terminating recursive exec function with a loop.

    Reported by @matthias-brun.

    opened by utaal 9
  • introduce open_invariant

    introduce open_invariant

    This introduces the Invariant<V> proof type and the open_invariant feature.

    Invariant<V> (defined in pervasive/invariants.rs) is defined to have two methods:

    #[proof]
    impl<V> Invariant<V> {
        #[verifier(pub_abstract)]
        #[spec]
        pub fn inv(&self, _v: V) -> bool
    
        #[verifier(pub_abstract)]
        #[spec]
        pub fn namespace(&self) -> int
    }
    

    The inv, of course, specifies what the invariant actually is, while namespace helps us ensure we don't open more than one invariant at once.

    The open_invariant feature is used like this:

      #[proof] let i : Invariant<V> = ...
    
      open_invariant!(&i => inner => {
        // assume that i.inv(inner) holds at the beginning
        // ...
        // assert that i.inv(inner) holds at the end
      });
    

    Rust -> VIR

    The open_invariant! macro is defined as follows:

    #[macro_export]
    macro_rules! open_invariant {
        ($eexpr:expr => $iident:ident => $bblock:block) => {
            #[verifier(invariant_block)] {
                #[allow(unused_mut)] let (guard, mut $iident) = crate::open_invariant_begin($eexpr);
                $bblock
                crate::open_invariant_end(guard, $iident);
            }
        }
    }
    

    When Verus sees the #[verifier(invariant_block)] attribute, it checks that the AST is EXACTLY the code that would be generated by the macro, to make sure the user doesn't cheat. It then converts it into an OpenInvariant node in VIR.

    Modes

    The Invariant<V> must of course be a proof variable. Right now, all code inside the invariant block is expected to be proof code, although later we will add the ability to have a single atomic exec instruction. We check the control flow to make sure it is not possible to early-exit out of an invariant block.

    Verification conditions

    Generating asserts and assumes for i.inv(inner) is the easy part.

    The more interesting part is to make sure the user does not open the same invariant twice. It is occasionally useful to open more than one invariant at once. In LinearDafny, this ability is very restricted: nested invariants must be right next to each other. To enforce this, we also require that invariants may only be opened in a non-ghost method, and only ghost code can be inside the invariant (so you can't have nested invariant across method boundaries).

    To support nested invariants more generally, the user can declare on a function what invariants it may open. The default is that an exec fn can open any invariant, while a proof fn cannot open any invariant. However, the user can override this with opens_invariants_any() or opens_invariants_none() in the header of a function.

    I also added headers for opens_invariants(..) and opens_invariants_except(..) so the user can specify an arbitrary set. However, these aren't implemented right now.

    See vir/src/inv_masks.rs for details on the VC generation. I aimed to not have it generate any extra VCs in the common case.

    Error messages

    Here are some example error messages (rust_verify/example/invariants.rs):

    Verifying root module
    error: possible invariant collision
      --> rust_verify/example/invariants.rs:37:20
       |
    37 |   open_invariant!(&i => inner => {
       |                    ^ this invariant
    38 |     open_invariant!(&i => inner2 => {
       |                      ^ might be the same as this invariant
    
    error: Cannot show invariant holds at end of block
      --> rust_verify/example/invariants.rs:57:34
       |
    57 |     open_invariant!(&i => inner => {
       |  __________________________________^
    58 | |     inner = 1;
    59 | |   });
       | |___^
    
    error: callee may open invariants disallowed at call-site
      --> rust_verify/example/invariants.rs:94:20
       |
    94 |   open_invariant!(&i => inner => {
       |                    ^ invariant opened here
    95 |     callee_mask_full(); // ERROR
       |     ^^^^^^^^^^^^^^^^^^ might be opened again in this call
    
    error: Cannot show invariant namespace is in the mask given by the function signature
       --> rust_verify/example/invariants.rs:100:20
        |
    100 |   open_invariant!(&i => inner => { // ERROR
        |                    ^ invariant opened here
    
    error: aborting due to 4 previous errors; 1 warning emitted
    

    Outstanding issues

    • The way that the Invariant<V> type, as well as the inv and namespace functions, is kind of jank.
      • The SST -> AIR conversion actually hardcode the names of the inv and namespace functions (which are in pervasive), but arguably Invariant should be a special type in VIR, with inv and namespace being special nodes, so that the VIR is self-contained, and so that all name-hardcoding happens in the rust -> VIR conversion.
      • ~There's a bug where the AIR function declarations for the inv and namespace don't even get generated if the user doesn't explicitly reference them.~ This is now fixed.

    Things to handle in later PRs:

    • Need to implement the more general headers for specifying which invariants a function might open.
    • Need to add a notion of an atomic instruction.
    opened by tjhance 8
  • Question about implementing a Map-like struct for exec mode

    Question about implementing a Map-like struct for exec mode

    According to https://github.com/secure-foundations/verus/issues/167, I was trying to implement a Map-like struct for exec mode. I checked how vec is implemented and was trying to implement a dict as a wrapper around std::HashMap. However, when I define the dict struct with generics, it seems that I cannot specify bounds. More details below:

    xudongs@xudongs-a01:~/verus/source:$ cat rust_verify/example/myexample.rs                   
    #[verifier(external_body)]
    pub struct Dict<#[verifier(maybe_negative)] K: std::cmp::Eq + std::hash::Hash, #[verifier(strictly_positive)] V> 
    {
        pub dict: std::collections::HashMap<K, V>,
    }
    
    fn main() { }
    
    xudongs@xudongs-a01:~/verus/source:$ ./tools/rust-verify.sh rust_verify/example/myexample.rs
    error: not yet supported: bounds on datatype parameters
      --> rust_verify/example/myexample.rs:21:1
       |
    21 | / pub struct Dict<#[verifier(maybe_negative)] K: std::cmp::Eq + std::hash::Hash, #[verifier(strictly_pos...
    22 | | {
    23 | |     pub dict: std::collections::HashMap<K, V>,
    24 | | }
       | |_^
    
    error: aborting due to previous error
    

    I have also tried to use where clause to specify bounds but where is not supported as well.

    Could you please let me know what is the best way for me to specify bounds since it is necessary to implement a wrapper around HashMap? If this can work I am happy to implement a dict for exec mode and send a PR if it is needed.

    question 
    opened by marshtompsxd 7
  • `error: expression has mode proof, expected mode exec` when using pervasive::map

    `error: expression has mode proof, expected mode exec` when using pervasive::map

    Hi Verus developers,

    Thanks for building verus and I am currently using it to verify some programs I wrote before. But I encountered some confusing errors and need some help from you.

    I was running verus (./tools/rust-verify.sh rust_verify/example/myexample.rs) on the following program:

    #[allow(unused_imports)]
    mod pervasive;
    use pervasive::map::*;
    
    #[exec]
    pub enum Myenum {
        S1,
        S2,
    }
    
    #[exec]
    pub fn myfun(map: Map<i32, i32>) -> Myenum {
        if map.dom().contains(10) {
            return Myenum::S1;
        } else {
            return Myenum::S2;
        }
    }
    
    #[exec]
    fn main() { }
    

    and verus reports the error

    error: expression has mode proof, expected mode exec
      --> rust_verify/example/mini.rs:14:16
       |
    14 |         return Myenum::S1;
       |                ^^^^^^^^^^
    
    error: aborting due to previous error
    

    If I remove the usage of Map, the error will disappear. I am confused about why this error can happen as I have annotated every function and every enum with exec as shown in the code, and why it is related to the pervasive::map. I have read this doc: https://github.com/secure-foundations/verus/blob/main/source/docs/design/modes.md but still do not know what is the root cause of the error. Could you please let me know what is the best way to address this error?

    Thank you

    soundness question 
    opened by marshtompsxd 7
  • flesh out the atomic library to the extent possible

    flesh out the atomic library to the extent possible

    Flesh out the trusted wrapper around std::sync::Atomic

    There are a few things we can't yet support:

    • compare_exchange(_weak) because Verus doesn't yet support Result
    • fetch_nand because (as far as I can tell) Verus doesn't support bitwise negation
    opened by tjhance 7
  • Add #[verifier(unforgeable)] attribute for #[proof] structs

    Add #[verifier(unforgeable)] attribute for #[proof] structs

    There are - fundamentally - two different kinds of "proof" objects. One kind is basically like a normal struct, except that it gets erased. With such types, you build up a "struct" of smaller proof objects.

    The other kind is the "base" objects that represent permissions or some other fancy thing, e.g., Permission. To make it easier to handle this second type, I added an attribute #[verifier(unforgeable)] to declare such types.

    The rules:

    • An unforgeable datatype must be proof.
    • An unforgeable datatype can only have spec fields.
    • You cannot call a constructor of an unforgeable datatype (in proof mode)
    • You cannot modify a field of an unforgeable datatype.

    Here, I add the attribute, implement these rules, and add tests. Although, it doesn't look like there is any support at all for field updates yet, so there was nothing to do for the last step.

    opened by tjhance 7
  • Add a simplified version of the quantifier instantiation profiler (--use-internal-profiler)

    Add a simplified version of the quantifier instantiation profiler (--use-internal-profiler)

    It only counts instantiations, and reports counts for builtin/internal quantifiers.

    It is intended, mainly, to find potential issues with internal quantifiers, not as a tool for the end user.

    Sample output:

    note:  1. Quantifier constructor_accessor_axiom, instantiations: 144041
              at: datatype#225, instantiations: 31253
              at: datatype#63, instantiations: 14099
              at: datatype#54, instantiations: 14099
              at: datatype#57, instantiations: 14099
              at: datatype#60, instantiations: 14099
              at: datatype#235, instantiations: 8070
              at: datatype#232, instantiations: 8070
              at: datatype#21, instantiations: 3790
              at: datatype#216, instantiations: 2628
              at: datatype#219, instantiations: 2628
              at: datatype#138, instantiations: 1673
              at: datatype#126, instantiations: 1673
              at: datatype#144, instantiations: 1673
              at: datatype#135, instantiations: 1673
              at: datatype#141, instantiations: 1673
              at: datatype#147, instantiations: 1673
              at: datatype#129, instantiations: 1673
              at: datatype#132, instantiations: 1673
              at: datatype#69, instantiations: 1649
              at: datatype#96, instantiations: 1319
              at: datatype#99, instantiations: 1319
              at: datatype#93, instantiations: 1319
              at: datatype#203, instantiations: 1180
              at: datatype#200, instantiations: 1180
              at: datatype#36, instantiations: 1126
              at: datatype#39, instantiations: 1126
              at: datatype#42, instantiations: 1126
              at: datatype#45, instantiations: 1126
              at: datatype#27, instantiations: 692
              at: datatype#82, instantiations: 348
              at: datatype#85, instantiations: 348
              at: datatype#193, instantiations: 270
              at: datatype#163, instantiations: 270
              at: datatype#181, instantiations: 270
              at: datatype#187, instantiations: 270
              at: datatype#190, instantiations: 270
              at: datatype#178, instantiations: 270
              at: datatype#175, instantiations: 270
              at: datatype#169, instantiations: 270
              at: datatype#184, instantiations: 270
              at: datatype#172, instantiations: 270
              at: datatype#166, instantiations: 270
              at: datatype#75, instantiations: 261
              at: datatype#15, instantiations: 168
              at: datatype#209, instantiations: 164
              at: datatype#110, instantiations: 79
              at: datatype#113, instantiations: 79
              at: datatype#107, instantiations: 79
              at: datatype#255, instantiations: 73
              at: datatype#252, instantiations: 73
              at: datatype#242, instantiations: 9
              at: datatype#245, instantiations: 9
    
    note:  2. Quantifier prelude_u_clip, instantiations: 9315
              at: #9430!2, instantiations: 9315
    
    note:  3. Quantifier prelude_unbox_box_bool, instantiations: 8348
              at: #62, instantiations: 8348
    
    note:  4. Quantifier prelude_unbox_box_int, instantiations: 6871
              at: #68, instantiations: 6871
    
    note:  5. Quantifier internal_pervasive.seq.Seq.index.?_pre_post_definition, instantiations: 5850
              at: #9587!3, instantiations: 5850
    
    note:  6. Quantifier prelude_nat_clip, instantiations: 5063
              at: #10394!2, instantiations: 5063
    
    note:  7. Quantifier prelude_box_unbox_int, instantiations: 3221
              at: #90!1, instantiations: 3221
    
    [...]
    
    note: 19. Quantifier user_crate__aux_defs__Arch__inv_57, instantiations: 1286
              at: #32797!6, instantiations: 532
              at: #21116!1, instantiations: 243
              at: #23942!1, instantiations: 243
              at: #63864!1, instantiations: 114
              at: #64985!1, instantiations: 73
              at: #62422!1, instantiations: 57
              at: #39736!1, instantiations: 22
              at: #37492!1, instantiations: 2
       --> verified-nrkernel/page-table/aux_defs.rs:313:24
        |
    311 |           &&& self.layers.len() <= MAX_NUM_LAYERS
        |  _____________-
    312 | |         &&& forall|i:nat|
    313 | |             #![trigger self.entry_size(i)]
        | |                        ^^^^^^^^^^^^^^^^^^
    314 | |             #![trigger self.num_entries(i)]
        | |                        ^^^^^^^^^^^^^^^^^^^
    ...   |
    318 | |                 &&& self.entry_size_is_next_layer_size(i)
    319 | |             }
        | |_____________- Triggers selected for this quantifier
    
    note: 20. Quantifier internal_crate__pt_impl__l2_impl__GhostPageDirectoryEntry_box_axiom_definition, instantiations: 1281
              at: #1351, instantiations: 1281
    
    opened by utaal 6
  • mode-checking for struct patterns mismatches fields

    mode-checking for struct patterns mismatches fields

    This wrongly passes mode-checking (it actually fails due to an unrelated erasure error, but this issue isn't about that):

    struct Foo {
        #[spec] a: u64,
        #[proof] b: u64,
    }
    
    #[proof]
    fn some_call(#[spec] x: u64, #[proof] y: u64) { }
    
    #[proof]
    fn t() {
        #[proof] let foo = Foo { a: 5, b: 6 };
        #[proof] let Foo { b, a } = foo;
    
        some_call(b, a);
    }
    

    Meanwhile, this wrongly gives an error:

    #[proof]
    fn t() {
        #[proof] let foo = Foo { a: 5, b: 6 };
        #[proof] let Foo { b, a } = foo;
    
        some_call(a, b);
    }
    
    error: expression has mode spec, expected mode proof
      --> test_pat_destruct.rs:26:18
       |
    26 |     some_call(a, b);
       |                  ^
    

    It looks like mode-checking doesn't handle let Foo { b, a } correctly because the order of its fields are wrong.

    bug soundness 
    opened by tjhance 0
  • Refactor ghost erasure and tracked variables

    Refactor ghost erasure and tracked variables

    (Initial, incomplete commit for draft pull request) This completely changes the way Verus erases ghost code for compilation and the way Verus checks tracked variable lifetimes in ghost code. In the long run, this should allow us to completely replace the old ghost code erasure with the proposed ghost block extension to rustc. In the short run, this should allow us to replace the old ghost code erasure with a macro-based erasure that would remove the last major dependency on our fork of rustc, hopefully allowing us to use a standard rustc build. It also simplifies the way programmers use tracked variables (at least compared to the current rules for tracked).

    I'll first review the old erasure mechanism, then discuss the long-term proposal, then the short-term proposal.

    Note: lifetime_generate.rs is still incomplete in this draft commit.

    Old Erasure Mechanism

    The old mechanism was based on the observation that only exec code gets compiled, and only exec and proof code get borrow checked:

    exec/proof/spec code --> type-check, mode-check
    exec/proof code      --> type-check, mode-check --> lifetime (borrow) check
    exec code            --> type-check, mode-check --> lifetime (borrow) check --> compile
    

    Based on this, the old mechanism erased spec code after type checking and then erased proof code after lifetime checking, leaving only exec code at the end to compile. However, while this erasure was simple in principle, it was difficult to erase code in Rust's HIR/THIR/MIR formats without making major changes to rustc. Therefore, the actual implementation ran rustc three times, erasing different amounts of code near the rustc front end (in Rust's AST format) in each of the three runs. This had many drawbacks:

    1. Running rustc multiple times was slow. In particular, users only get verification feedback after the 2nd run, doubling the startup latency for verification.
    2. Mode checking operated on Rust HIR, while ghost code erasure operated on Rust AST. To connect AST to HIR, we used spans, which was a hack.
    3. We needed our own fork of rustc.
    4. Running rustc three times with slightly different AST meant that type inference was not always consistent between the three runs. Users often had to add extra type annotations to make sure that type checking succeeded in all three runs.

    Ghost blocks

    There has been a recent proposal to add ghost code as a Rust feature. This proposal is syntactic: ghost code is marked explicitly in the source code by writing the ghost code in "ghost blocks". In addition, ghost variables may be represented using new types (e.g. x: Ghost<t> instead of ghost x: t). This proposal would have the advantage of requiring only one run of rustc, rather than three.

    To prepare for this proposal, Verus has been switching over the last year to a more coarse-grained, syntactic style of expressing ghost code. For example, calls from exec code to proof lemmas are now written inside of explicit proof blocks. In fact, the syntactic distinction between exec and non-exec code may help make the code more readable. However, we've also moved to a style that syntactically distinguishes between proof code and spec code, and this seems to be just annoying. In particular, proof (tracked) arguments to functions have to be marked as tracked, as in let z = f(tracked x, tracked y); rather than just let z = f(x, y);. The mode checker can easily infer these tracked annotations, but unfortunately, the mode checking runs after the syntactic splitting into exec/proof/spec code, when it's too late for the mode inference to help:

    all code --> split into exec/proof/spec --> type-check, mode-check --> lifetime (borrow) check --> compile
    

    Long-term proposal

    To make programming in Verus more pleasant, this pull request reduces the reliance on syntax to distinguish between the different code modes. Syntax is used to distinguish between exec and non-exec code (e.g. by using proof blocks), but not to distinguish between proof and spec code inside a proof block. This allows programmers to write let z = f(x, y); rather than let z = f(tracked x, tracked y);.

    This pull request assumes the simplest possible treatment of lifetime checking for non-exec code in the new ghost code Rust proposal: it assumes that the proposal skips lifetime checking of non-exec code entirely. This means that Verus needs to have a separate way to check lifetimes of proof code. This separate checking is the main purpose of this pull request. Roughly, this separate checking looks like the following:

    all code --> split into exec/non-exec --> type-check, mode-check --+--> lifetime (borrow) check exec code --> compile
                                                                       |
                                                                       +--> lifetime (borrow) check proof code
    

    In this design, rustc performs type checking of all code, Verus performs mode checking of all code, rustc performs lifetime checking and compilation of exec code, and Verus performs lifetime checking of proof code. (Verus will only need to check proof code that manipulates tracked data; proofs that work only on spec data trivially satisfy lifetime constraints.)

    This pull request implements Verus's lifetime checking for proof code. This checking no longer uses erase.rs and erase_rewrite.rs; these will eventually be deleted entirely. Instead, the checking works by constructing synthetic Rust code to abstractly represent the various actions that the proof code performs, such as variable declarations, function calls, and pattern matching. It then sends this synthetic code to a separate invocation of rustc. (We could also consider using some other checker, such as Polonius, although generating synthetic Rust code is fairly easy.) Since this synthetic code generation occurs after mode inference, it can use the infered modes to guide the synthetic code generation.

    This approach should solve the drawbacks listed above:

    1. It only requires running rustc once on the original source code. (It does internally invoke rustc a second time, but only on a relatively small amount of synthetic code with no macros. Furthermore, when verifying only one function at a time, we could configure Verus to only run the lifetime checking only on the function being verified.)
    2. Both the mode checking and proof lifetime checking operate on Rust HIR/THIR, not on Rust AST. There's no need to connect AST to HIR with spans.
    3. Assuming the ghost code proposal becomes part of Rust, there will be no need to use our own fork of rustc. (Until this happens, we might need our own fork of rustc to implement the ghost code proposal. However, see the short-term proposal below for a way to use the standard rustc even without the ghost code proposal.)
    4. Type checking happens only once, so there's no chance for multiple runs of rustc to encounter different type inference results. Full type information is available to when generating the synthetic code, so it doesn't need to rely on type inference.

    Short-term proposal

    The long-term proposal relies on the ghost code proposal being implemented and adopted. Before that happens, we can still use this pull request's proof-mode lifetime checking in a slightly hackier way to get some of the benefits of the long-term proposal. In particular, the short-term proposal should be able to run with standard rustc, without relying on a separate fork of rustc, and the short-term proposal will allow programmers to write let z = f(x, y); rather than let z = f(tracked x, tracked y);. The short-term proposal still requires running rustc multiple times on the original source code, although only twice instead of three times:

    exec code --> type-check, mode-check --> lifetime (borrow) check exec code --> compile
    
    all code --> type-check, mode-check --+
                                          |
                                          +--> lifetime (borrow) check proof code
    

    To avoid needing a fork of rustc, the short-term proposal uses the syntax macro to erase non-exec code, rather than relying on erase.rs and erase_rewrite.rs.

    This pull request implements the short-term proposal in its entirety (when using the new command-line option --erasure macro), except that there are still a few leftover unrelated dependencies of Verus on our rustc fork, so it doesn't completely get us off the fork. But the remaining dependencies should be relatively easy to remove.

    Possible alternative (even longer-term): MIR rewriting

    There may be other ways to implement erasure and lifetime checking for Verus. For example, if rustc exposes an official interface to read and transform "stable" MIR code, we might be able to use MIR transformation to erase spec code and proof code. However, this support is likely to be further in the future. It would also mean we'd have to perform MIR transformations based on HIR mode checking, although we might be able to rerun the mode checking at the MIR level. I think we should continue to think about alternatives like this, but my hope is that this pull request can improve our lives in the short term regardless of future alternatives.

    Ghost types

    Right now, our ghost types (Ghost<T> and Tracked<T>) are somewhat annoying because programmers have to explicitly dereference them to retrieve the underlying T ghost value. This is an orthogonal issue to this pull request, and this pull request does not address it. However, as part of the long-term solution above, but separately from this pull request, we could try to integrate some automatic dereferencing of ghost types into the ghost code proposal, so that the Rust type checker could automatically add the necessary dereferencing. As far as I recall, something like this was part of the original discussion for ghost code, although I don't think the details were ever settled.

    Implementation details

    Order of operations: long-term proposal

    As discussed above, in the long-term proposal, rustc would only run once on the original source code. Here's what would happen during that one run:

    1. Parsing, AST construction, macro expansion (including the Verus syntax macro)
    2. Rust AST -> HIR
    3. Rust's type checking
    4. Rust HIR/THIR -> MIR conversion
    5. Rust's lifetime/borrow checking for non-ghost (exec) code
    6. Verus HIR/THIR -> VIR conversion
    7. Verus mode checking
    8. Verus lifetime/borrow checking for proof code (by generating synthetic code and running rustc)
    9. Verus SMT solving
    10. Rust compilation to machine code (if --compile is set)

    Order of operations: short-term proposal

    The order of operations in the short-term proposal is a little messier because we have to run rustc twice on the original source code, once erasing ghost code and once keeping ghost code. This causes some tension in the order of operations: it would be cleanest to run one rustc invocation entirely, and then the other entirely, but this would be slow. For example, if we ran the ghost-erase rustc and then the ghost-keep rustc, then the user would receive no feedback about verification until parsing and macro expansion for both rustc invocations had completed. On the other hand, if we ran the ghost-keep rustc and then the ghost-erase rustc, the user would receive no feedback about lifetime/borrow checking errors in exec functions until verification had completed. So for better latency, the implementation interleaves the two rustc invocations (marked GHOST for keeping ghost and EXEC for erasing ghost):

    1. GHOST: Parsing, AST construction, macro expansion (including the Verus syntax macro, keeping ghost code)
    2. GHOST: Rust AST -> HIR
    3. GHOST: Rust's type checking
    4. GHOST: Verus HIR/THIR -> VIR conversion
    5. GHOST: Verus mode checking
    6. GHOST: Verus lifetime/borrow checking for both proof and exec code (by generating synthetic code and running rustc), but delaying any lifetime/borrow checking error messages until EXEC has a chance to run
    7. GHOST: If there were no lifetime/borrow checking errors, run Verus SMT solving
    8. EXEC: Parsing, AST construction, macro expansion (including the Verus syntax macro, erasing ghost code)
    9. EXEC: Rust AST -> HIR
    10. EXEC: Rust's type checking
    11. EXEC: Rust HIR/THIR -> MIR conversion
    12. EXEC: Rust's lifetime/borrow checking for non-ghost (exec) code
    13. GHOST: If there were no EXEC errors, but there were GHOST lifetime/borrow checking errors, print the GHOST lifetime/borrow checking errors
    14. EXEC: Rust compilation to machine code (if --compile is set)

    To avoid having to run the EXEC lifetime/borrow checking before verification, which would add latency before verification, the short-term implementation takes advantage of the synthetic-code lifetime/borrow checking to perform a quick early test for lifetime/borrow checking on all code (even pure exec code). If this detects any lifetime/borrow errors, the implementation skips verification and gives the EXEC rustc a chance to run, on the theory that the EXEC rustc will generate better error messages for any exec lifetime/borrow errors. The GHOST lifetime/borrow errors are printed only if the EXEC rustc finds no errors. Note that the long-term proposal avoids this complex interleaving; rustc only runs once on the original source code, and it naturally prints the regular exec lifetime/borrow checking errors first without so much effort. The long-term proposal also doesn't require the synthetic code generation for all functions; it only requires it for proof code.

    Synthetic code generation

    The --log-all or --log-lifetime options will print the generated synthetic code for lifetime/borrow checking to a file in the .verus-log directory. For the following example source code:

    struct S {
    }
    
    spec fn fspec(i: u32, s1: S, s2: S) -> u32 {
        i
    }
    
    proof fn fproof(i: u32, tracked s1: S, tracked s2: S) -> u32 {
        i
    }
    
    fn fexec(i: u32, s1: S, s2: S) -> u32 {
        i
    }
    
    proof fn test_proof(i: u32, tracked s: S) {
        let j = fspec(i, s, s);
        let k = fproof(i, s, s);
    }
    
    fn test_exec(i: u32, s: S)
        requires i < 100
    {
        proof {
            let j = fspec(i, s, s);
            let k = fproof(i, s, s);
        }
        let k = fexec(i, s, s);
        let n = fexec(i, s, s);
    }
    

    the following crate-lifetime.rs file is generated when logging is enabled:

    #![allow(non_camel_case_types)]
    fn f<A, B>(a: A) -> B { unimplemented!() }
    fn notdet() -> bool { unimplemented!() }
    
    struct T_0_S {
    }
    
    fn x_3_f_fexec_3(
        x_1_i: u32,
        x_4_s1: T_0_S,
        x_5_s2: T_0_S,
    ) -> u32
    {
        x_1_i
    }
    
    fn x_8_f_test_proof_8(
        x_6_s: T_0_S,
    )
    {
        f::<_, u32>((x_6_s, x_6_s, ));
    }
    
    fn x_12_f_test_exec_12(
        x_1_i: u32,
        x_6_s: T_0_S,
    )
    {
        {
            f::<_, u32>((x_6_s, x_6_s, ));
        };
        let x_9_k = f::<_, u32>((x_1_i, x_6_s, x_6_s, ));
        let x_10_n = f::<_, u32>((x_1_i, x_6_s, x_6_s, ));
    }
    

    Note that the synthetic code erases all spec code, keeping only exec and proof code. Also note that the synthetic code is simpler than the original source code in various ways, keeping only what's necessary for lifetime/borrow checking. For example, all function calls are redirected to the same function f, which takes the function arguments as a tuple.

    Error reporting

    As discussed in the order of operations above, the implementation lets rustc print any exec-mode lifetime/borrow errors first. In the example above, this results in the following output:

    error[E0382]: use of moved value: `s`
       --> .\rust_verify\example\features.rs:210:25
        |
    203 | fn test_exec(i: u32, s: S)
        |                      - move occurs because `s` has type `S`, which does not implement the `Copy` trait
    ...
    210 |     let k = fexec(i, s, s);
        |                      -  ^ value used here after move
        |                      |
        |                      value moved here
    
    error[E0382]: use of moved value: `s`
       --> .\rust_verify\example\features.rs:211:22
        |
    203 | fn test_exec(i: u32, s: S)
        |                      - move occurs because `s` has type `S`, which does not implement the `Copy` trait
    ...
    210 |     let k = fexec(i, s, s);
        |                         - value moved here
    211 |     let n = fexec(i, s, s);
        |                      ^ value used here after move
    
    error[E0382]: use of moved value: `s`
       --> .\rust_verify\example\features.rs:211:25
        |
    203 | fn test_exec(i: u32, s: S)
        |                      - move occurs because `s` has type `S`, which does not implement the `Copy` trait
    ...
    211 |     let n = fexec(i, s, s);
        |                      -  ^ value used here after move
        |                      |
        |                      value moved here
    
    error: aborting due to 3 previous errors
    

    If we change the example by commenting out the exec-mode error lines, then we will see the ghost-mode lifetime/borrow errors from the synthetic code:

    error: use of moved value: `s`
       --> .\rust_verify\example\features.rs:198:37
        |
    198 | proof fn test_proof(i: u32, tracked s: S) {
        |                                     ^
    199 |     let j = fspec(i, s, s);
    200 |     let k = fproof(i, s, s);
        |                       ^  ^
    
    error: use of moved value: `s`
       --> .\rust_verify\example\features.rs:203:22
        |
    203 | fn test_exec(i: u32, s: S)
        |                      ^
    ...
    208 |         let k = fproof(i, s, s);
        |                           ^  ^
    
    error: aborting due to 2 previous errors
    

    The error messages here aren't quite as detailed, but hopefully they should be adequate. The ghost-mode messages are generated by running rustc on the synthetic code, with rustc configured to generate errors in JSON format, capturing the JSON errors and parsing them to retrieve the line/column information and error messages, converting the synthetic line/column information back into spans for the original source code, and then sending the error messages and spans to the rustc diagnostics for the original source code. It's possible that if we used, say, Polonius, instead of rustc, to do the lifetime/borrow checking, we wouldn't have to jump through so many hoops to manage the line/column/span information in the errors, but I haven't looked into this.

    opened by Chris-Hawblitzel 0
  • Can't use nat in const context

    Can't use nat in const context

    This code

    #![allow(unused_imports)]
    use builtin::*;
    use builtin_macros::*;
    
    verus! {
    pub const RC_WIDTH: nat = 4 as nat;
    }
    
    fn main() { }
    

    results in

    error[E0605]: non-primitive cast: `i32` as `builtin::nat`
     --> rust_verify/example/scache/testcase.rs:6:27
      |
    6 | pub const RC_WIDTH: nat = 4 as nat;
      |                           ^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object
    
    error: aborting due to previous error
    
    For more information about this error, try `rustc --explain E0605`.
    verification results:: verified: 0 errors: 0
    
    question 
    opened by jonhnet 1
  • missed case in recursive type-checking

    missed case in recursive type-checking

    This ought to fail the type well-founded-recursion-checking, but it doesn't:

    struct Qux<T> {
        t: T,
    }
    
    struct Bar<T> {
        s: Box<Set<Qux<T>>>,
    }
    
    struct Foo {
        bar: Bar<Foo>,
    }
    

    e.g. you can do the usual russell's paradox set-of-sets-that-do-not-contain themselves:

    proof fn prove_false()
        ensures false
    {
        let s = Set::<Qux<Foo>>::new(
            |qux: Qux<Foo>| !qux.t.bar.s.contains(qux)
        );
        let qux = Qux{ t: Foo { bar: Bar { s: box s } } };
        assert(qux.t.bar.s.contains(qux));
        assert(!qux.t.bar.s.contains(qux));
        assert(false);
    }
    
    soundness 
    opened by tjhance 0
  • Very simple fix for `Box::new()`

    Very simple fix for `Box::new()`

    This PR simply changes the path for Box::new(), from std::box::Box::<T>::new to std::boxed::Box::<T>::new

    Just for context: When I was using Box::new(), I encountered the error below.

    error: `alloc::boxed::{impl#0}::new` is not supported (note: currently Verus does not support definitions external to the crate, including most features in std)
    

    After correcting the path, this issue seems to be resolved.

    opened by channy412 0
  • Panic when spec function used in exec context

    Panic when spec function used in exec context

    The program

    pub struct Lock {}
    
    #[is_variant]
    pub enum OptionX<T> {
        NoneX,
        SomeX(T)
    }
    
    pub fn what_is_wrong() -> bool
    {
        let opt_lock = OptionX::SomeX(Lock{});
        let lock = opt_lock.get_SomeX_0();   // This line triggers panic
        true
    }
    

    gives the error:

    thread 'rustc' panicked at 'internal error: expected Some(Expr) src/linear/linear.rs:60:16: 60:38 (#23505)', rust_verify/src/erase.rs:431:28
    

    Mode-checking algorithm should catch this and present a nicer error message.

    bug 
    opened by TonyZhangND 0
Owner
Secure Foundations Lab
We investigate long-term, fundamental improvements in how to design and build secure systems.
Secure Foundations Lab
Statically verified Rust struct field names as strings.

field Statically verified struct field names as strings. See the documentation for more details. Installation Add the following to your Cargo manifest

Indraneel Mahendrakumar 3 Jul 9, 2023
F-Fetch targets low systems. Written in Rust. It's very simple, designed so you can pick it up and replace it.

F-Fetch F-Fetch targets low systems. Written in Rust. It's very simple, designed so you can pick it up and replace it. First Look ~/.config/ffetch/con

cd 3 Jul 10, 2023
A low-level ncurses wrapper for Rust

ncurses-rs This is a very thin wrapper around the ncurses TUI lib. NOTE: The ncurses lib is terribly unsafe and ncurses-rs is only the lightest wrappe

Jeaye Wilkerson 628 Jan 7, 2023
Horus is an open source tool for running forensic and administrative tasks at the kernel level using eBPF, a low-overhead in-kernel virtual machine, and the Rust programming language.

Horus Horus is an open-source tool for running forensic and administrative tasks at the kernel level using eBPF, a low-overhead in-kernel virtual mach

null 4 Dec 15, 2022
Rust low-level minimalist APNG writer and PNG reader with just a few dependencies with all possible formats coverage (including HDR).

project Wiki https://github.com/js29a/micro_png/wiki at glance use micro_png::*; fn main() { // load an image let image = read_png("tmp/test.

jacek SQ6KBQ 8 Aug 30, 2023
A new pure-Rust library for cross-platform low-level access to USB devices.

nusb A new pure-Rust library for cross-platform low-level access to USB devices. Documentation Compared to rusb and libusb Pure Rust, no dependency on

Kevin Mehall 23 Oct 30, 2023
A low-level MVCC file format for storing blobs.

Sediment This repository isn't ready for public consumption. It just reached a stage where I wanted to start sharing ideas with others as well as usin

Khonsu Labs 24 Jan 8, 2023
Unopinionated low level API bindings focused on soundness, safety, and stronger types over raw FFI.

?? firehazard ?? Create a fire hazard by locking down your (Microsoft) Windows so nobody can escape (your security sandbox.) Unopinionated low level A

null 5 Nov 17, 2022
Simple low-level web server to serve file uploads with some shell scripting-friendly features

http_file_uploader Simple low-level web server to serve file uploads with some shell scripting-friendly features. A bridge between Web's multipart/for

Vitaly Shukela 2 Oct 27, 2022
Low level access to processors using the AArch64 execution state.

aarch64-cpu Low level access to processors using the AArch64 execution state. Usage Please note that for using this crate's register definitions (as p

Rust Embedded 18 Jan 5, 2023
High-performance, low-level framework for composing flexible web integrations

High-performance, low-level framework for composing flexible web integrations. Used mainly as a dependency of `barter-rs` project

Barter 8 Dec 28, 2022
Wrapper around atspi-code to provide higher-level at-spi Rust bindings

atspi Wrapper around atspi-codegen to provide higher-level at-spi Rust bindings. Contributions Take a look at our atspi-codegen crate, and try inpleme

Odilia 3 Feb 7, 2022
Low overhead Rust implementation of time-related concepts

It's Rust time! Low overhead implementation of time-related concepts. Who is time for? For applications where simplicity and low-overhead are more imp

MOIA GmbH - Open Source 10 Feb 20, 2023
Dog command for *nix systems, Rust port of dog.

dog-rs Dog command for *nix systems, Rust port of dog. Because there is a cat command, should be a dog command too. It was written completely using VS

Juanjo Salvador 2 Sep 29, 2021
Open-source Rust framework for building event-driven live-trading & backtesting systems

Barter Barter is an open-source Rust framework for building event-driven live-trading & backtesting systems. Algorithmic trade with the peace of mind

Barter 157 Feb 18, 2023
🛠️ An experimental functional systems programming language, written in Rust and powered by LLVM as a backend.

An experimental functional systems programming language, written in Rust, and powered by LLVM as a backend. ?? Goal: The intent is to create a program

codex 3 Nov 15, 2023
Tool to draw low-resolution graphs in terminal

lowcharts Tool to draw low-resolution graphs in terminal. lowcharts is meant to be used in those scenarios where we have numerical data in text files

juanleon lahoz 114 Dec 31, 2022
Enhance low quality images and videos using AI technology.

Real ESRGAN GUI Real ESRGAN GUI is a simple and minimal GUI for xinntao's Real-ESRGAN This allows you to enhance low quality images and videos using A

null 20 Dec 21, 2022
The hacker's BLE (bluetooth low energy) browser terminal app

Blendr is a terminal UI app for browsing BLE (Bluetooth Low Energy) devices. It allows you to inspect, search, connect, and analyze data coming from B

Dmitriy Kovalenko 120 Jul 4, 2023