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

Prepare miri engine for enforcing validity invariant during execution #54762

Merged
merged 19 commits into from
Oct 9, 2018

Conversation

RalfJung
Copy link
Member

@RalfJung RalfJung commented Oct 2, 2018

In particular, make recursive checking of references optional, and add a const_mode parameter that says whether usize is allowed to contain a pointer. Also refactor validation a bit to be type-driven at the "leafs" (primitive types), and separately validate scalar layout to catch NonNull violations (which it did not properly validate before).

Fixes #53826
Also fixes #54751

r? @oli-obk

@rust-highfive
Copy link
Collaborator

⚠️ Warning ⚠️

  • These commits modify submodules.

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Oct 2, 2018
@RalfJung
Copy link
Member Author

RalfJung commented Oct 2, 2018

We have an interesting test failure: This extern static is considered not sufficiently aligned

#[link_name = "check_static_recursion_foreign_helper"]
extern "C" {
    #[allow(dead_code)]
    static test_static: c_int;
}

static B: &'static c_int = unsafe { &test_static };

Seems like we set some rather arbitrary alignment for these? Ideally we'd pick an alignment based on the type, would that make sense? Where is the code deciding that?

@RalfJung
Copy link
Member Author

RalfJung commented Oct 2, 2018

For now, I made it skip pointers to foreign statics entirely, not even checking their alignment. I think we could do better, but this is not a regression.

@rust-highfive

This comment has been minimized.

src/librustc_mir/interpret/terminator.rs Outdated Show resolved Hide resolved

/// Make sure that `value` matches the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the....."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see value as the subject in this sentence, so it shouldn't need a "the". It's like a name: "Make sure that Berlin matches..."

trace!("validate scalar by layout: {:#?}, {:#?}, {:#?}", value, size, layout);
let (lo, hi) = layout.valid_range.clone().into_inner();
if lo == u128::min_value() && hi == u128::max_value() {
// Nothing to check
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about undef checks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we or do we not want to allow undef when the range is the entire possible range? That would mean validate_scalar_layout would also have to take a const_mode.

Note that we have type-based checks for that later, where we decide on a type-by-type basis whether we want undef or not. This here is only for additional restrictions that may be imposed on top of what the primitive types say.

) -> EvalResult<'tcx> {
trace!("validate scalar by layout: {:#?}, {:#?}, {:#?}", value, size, layout);
let (lo, hi) = layout.valid_range.clone().into_inner();
if lo == u128::min_value() && hi == u128::max_value() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically we should be checking whether hi.overflowing_add(1) == lo, because there are 2^128 possible ways to encode the full range

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not correct either, we actually pick the range depending on the size of the scalar. I am now using

let max_hi = u128::max_value() >> (128 - size.bits()); // as big as the size fits

src/librustc_mir/interpret/validity.rs Outdated Show resolved Hide resolved
Scalar::Ptr(_) => {
// Comparing a ptr with a range is not meaningfully possible.
// In principle, *if* the pointer is inbonds, we could exclude NULL, but
// that does not seem worth it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly sure I had a test for just this case (enum Foo { E = 0 } and then transmuting a pointer to the enum type)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this more strict with the latest changes, could you have a look?

I found your test in ub-enum.rs. Bot it kept failing as expected... maybe because this is actually not considered a primitive type, but an enum, and hence it loads the discriminant and that always fails when it is a pointer?

src/librustc_mir/interpret/validity.rs Outdated Show resolved Hide resolved
src/test/ui/consts/const-eval/ub-nonnull.stderr Outdated Show resolved Hide resolved
@RalfJung
Copy link
Member Author

RalfJung commented Oct 3, 2018

I finally found a way to unify handling of thin and fat pointers, so I could not resist adding that to this PR.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 3, 2018

Seems like we set some rather arbitrary alignment for these? Ideally we'd pick an alignment based on the type, would that make sense? Where is the code deciding that?

Actually we do not set any alignment for these foreign statics, we get a "dangling pointer" error even when just trying to check alignment. For now, I think it's best to keep ignoring them.

}
static CRASH: () = symbol;
static CRASH: u32 = symbol;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to change this test because "reading" a () does not actually read anything...

@RalfJung
Copy link
Member Author

RalfJung commented Oct 4, 2018

Might be worth doing a perf run.

@bors try

@bors
Copy link
Contributor

bors commented Oct 4, 2018

⌛ Trying commit 5dfc8f1 with merge 98f2e1b...

bors added a commit that referenced this pull request Oct 4, 2018
Prepare miri engine for enforcing validity invariant during execution

In particular, make recursive checking of references optional, and add a `const_mode` parameter that says whether `usize` is allowed to contain a pointer. Also refactor validation a bit to be type-driven at the "leafs" (primitive types), and separately validate scalar layout to catch `NonNull` violations (which it did not properly validate before).

Fixes #53826
Also fixes #54751

r? @oli-obk
@bors
Copy link
Contributor

bors commented Oct 4, 2018

☀️ Test successful - status-travis
State: approved= try=True

@RalfJung

This comment has been minimized.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 4, 2018

@rust-timer build 98f2e1b

@rust-timer
Copy link
Collaborator

Success: Queued 98f2e1b with parent c67ea54, comparison URL.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 4, 2018

Unexpectedly things got a bit slower (because now it does that scalar check quite more often than it used to). It's 5% only for very short benchmarks though (clean incremental), and more around 1-2% for the stress tests.

Scalar::Ptr(ptr) => {
if lo == 1 && hi == max_hi {
// only NULL is not allowed.
// We can call `check_align` to check non-NULL-ness, but have to also look
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how a pointer with an actual (dead or live) allocation could ever be null.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you offset a pointer enough, it can overflow to NULL.

The only way we can know it is not NULL is to make sure it is inbounds.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you overflow it far enough so it is inbounds again, won't we have the same problem?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would that be a problem? The overflow itself is okay. We only allow overflow when using wrapping_offset.

self.memory.get_fn(ptr).is_ok();
if !non_null {
// could be NULL
return validation_failure!("a potentially NULL pointer", path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a test if this is reachable at all, otherwise, remove

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is currently unreachable because we have no way in CTFE to add an offset to a pointer. It will be reachable once that is a possibility.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 9, 2018

The only actually surprising perf regression seems to be syn, and that one I unfortunately cannot reproduce locally... Also note syn has a ? indicating it has high variance.

"clean-opt" is supposed to regress 3%, it's 1% here (and 1% I have found impossible to debug, there's just too much noise). Looking at perf report, this spends almost all its time in LLVM. No idea how these changes here should affect anything.

I also tried reproducing "patched incremental: println-opt" for syn, but I am getting vastly different numbers for the instruction count (on the order of 47 billion instead of 27 billion), so I must be doing something else. On those measurements, this patch is a <0.1% slowdown. Here's the commands I used:

# prepare
export CARGO_INCREMENTAL=1
git reset --hard HEAD && rm target -rf && cargo +stage2.2 build --release
# bench
patch -p1 < 0-println.patch && perf stat -- cargo +stage2.2 build --release

@RalfJung
Copy link
Member Author

RalfJung commented Oct 9, 2018

I managed to get the perf collector running locally, and used it to re-run the syn benchmarks. I am again seeing numbers around 45 billion instead of the 27 billion on the website, and I am seeing a regression of around 0.1%. I'd call that noise.

I can't think of anything else I could do.

@oli-obk
Copy link
Contributor

oli-obk commented Oct 9, 2018

@bors r+

@bors
Copy link
Contributor

bors commented Oct 9, 2018

📌 Commit fe96f82 has been approved by oli-obk

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Oct 9, 2018
@bors
Copy link
Contributor

bors commented Oct 9, 2018

⌛ Testing commit fe96f82 with merge 0e07c42...

bors added a commit that referenced this pull request Oct 9, 2018
Prepare miri engine for enforcing validity invariant during execution

In particular, make recursive checking of references optional, and add a `const_mode` parameter that says whether `usize` is allowed to contain a pointer. Also refactor validation a bit to be type-driven at the "leafs" (primitive types), and separately validate scalar layout to catch `NonNull` violations (which it did not properly validate before).

Fixes #53826
Also fixes #54751

r? @oli-obk
@bors
Copy link
Contributor

bors commented Oct 9, 2018

☀️ Test successful - status-appveyor, status-travis
Approved by: oli-obk
Pushing 0e07c42 to master...

@bors bors merged commit fe96f82 into rust-lang:master Oct 9, 2018
@rust-highfive
Copy link
Collaborator

📣 Toolstate changed by #54762!

Tested on commit 0e07c42.
Direct link to PR: #54762

🎉 miri on windows: build-fail → test-pass.
🎉 miri on linux: build-fail → test-pass.

rust-highfive added a commit to rust-lang-nursery/rust-toolstate that referenced this pull request Oct 9, 2018
Tested on commit rust-lang/rust@0e07c42.
Direct link to PR: <rust-lang/rust#54762>

🎉 miri on windows: build-fail → test-pass.
🎉 miri on linux: build-fail → test-pass.
@alexcrichton
Copy link
Member

It looks like this may have caused a minor regression across a number of targets on perf

@RalfJung RalfJung deleted the miri-validate branch October 17, 2018 19:29
@RalfJung
Copy link
Member Author

RalfJung commented Oct 17, 2018

These targets that regressed all have large constants.

That'll likely be caused by us now checking both layout and type invariants. There is some redundancy in the checking there, which I am not sure how to avoid (while keeping the code somewhat reasonably organized).

@oli-obk
Copy link
Contributor

oli-obk commented Oct 17, 2018

Maybe we could not run the layout checks on those value where we know the type checks to be sufficient? E.g. on char, bool and basic integers

@RalfJung
Copy link
Member Author

You want to determine that by type? ;) I think you can add references and raw pointers to that list. (References have a layout restriction but we also check that and more in the type-based check.) In fact, for all the types which have a type-based check, that check should be sufficient.

Sure, worth a try I guess. I am not convinced it will help much but there is only one way to find out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Const sanity checks skips uninhabited arrays Refactor validate_scalar to take advantage of type information
6 participants