rust-lang / miri

An interpreter for Rust's mid-level intermediate representation
Apache License 2.0
4.33k stars 329 forks source link

Make sure interpreter errors are never discarded #3855

Open RalfJung opened 2 weeks ago

RalfJung commented 2 weeks ago

One important invariant of Miri is that when an interpreter error is raised (in particular a UB error), those must not be discarded: it's not okay to just check foo().is_err() and then continue executing.

This seems to catch new contributors by surprise fairly regularly. Would be good to make sure this can never happen. Ideally we'd have some sort of static analysis for this, but I can't think of an easy way to do that (could be an interesting clippy lint). The next best thing is to enforce this dynamically. The problem is that InterpError creation does not have access to the InterpCx. So instead we'd need some thread-local state to indicate "this interpreter is busted, don't continue executing in it... and we'd have to hope that nobody creates two interpreter instances on the same thread... but I can't think of a better way right now.

workingjubilee commented 1 week ago

drop bombs to discourage misuse?

RalfJung commented 1 week ago

Yeah, maybe that works.

tiif commented 6 days ago

I actually encountered this a few times so I'd really appreciate this enhancement, here is how I usually encounter this, hope it would help to produce a clearer diagnostic/ a test if anyone would like to work on this:

This usually happen when I forgot to put a ? for function that returns InterpResult, for example:

        match offset {
            None => fd.read(&fd, communicate, buf, count, dest, this), // the `?` is missing here
            Some(offset) => {
                let Ok(offset) = u64::try_from(offset) else {
                    let einval = this.eval_libc("EINVAL");
                    this.set_last_error(einval)?;
                    this.write_int(-1, dest)?;
                    return Ok(());
                };
                fd.pread(communicate, offset, buf, count, dest, this) // the `?` is missing here
            }
        };

(fd.read and fd.pread return InterpResult)

At this point, the compiler will emit an error and suggestion:

warning: unused `Result` that must be used
   --> src/shims/unix/fd.rs:587:9
    |
587 | /         match offset {
588 | |             None => fd.read(&fd, communicate, buf, count, dest, this),
589 | |             Some(offset) => {
590 | |                 let Ok(offset) = u64::try_from(offset) else {
...   |
597 | |             }
598 | |         };
    | |_________^
    |
    = note: this `Result` may be an `Err` variant, which should be handled
    = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
    |
587 |         let _ = match offset {
    |         +++++++

So if I didn't examine the suggestion carefully, I would just directly apply the suggestion and leads to:

        let _ = match offset { // the change is the `let _ = here`
            None => fd.read(&fd, communicate, buf, count, dest, this),
            Some(offset) => {
                let Ok(offset) = u64::try_from(offset) else {
                    let einval = this.eval_libc("EINVAL");
                    this.set_last_error(einval)?;
                    this.write_int(-1, dest)?;
                    return Ok(());
                };
                fd.pread(communicate, offset, buf, count, dest, this)
            }
        };

After the change, it will compile successfully. IMO this error is pretty dangerous as it can only be caught manually or by certain test, and the test error is not helpful for debugging as it points to different direction ( related discussion in zulip )

The correct fix for the initial code is actually:

        match offset {
            None => fd.read(&fd, communicate, buf, count, dest, this)?, // Added a `? `here
            Some(offset) => {
                let Ok(offset) = u64::try_from(offset) else {
                    let einval = this.eval_libc("EINVAL");
                    this.set_last_error(einval)?;
                    this.write_int(-1, dest)?;
                    return Ok(());
                };
                fd.pread(communicate, offset, buf, count, dest, this)? // Added a `?` here
            }
        };
RalfJung commented 6 days ago

Yeah IMO that's a dangerous suggestion to make.

CAD97 commented 4 days ago

FWIW diagnostics and compilation errors in rustc work in a similar fashion, and dropping one without emitting it will ICE ultimately via panic_any. Since this is a programmer error and not user error, panicking is a reasonable out.

Just make sure to check if you're already panicking to avoid a double panic abort.