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

Allow test suite to pass under miri, and fix various other miri/stacked borrow issues #134

Merged
merged 2 commits into from Jan 10, 2021

Conversation

thomcc
Copy link
Contributor

@thomcc thomcc commented Jan 7, 2021

There are a few things that anyhow does that are currently considered unsound by miri and stacked borrows, this fixes them to the best of my ability. After this, the test suite passes under miri. It also cases where miri didn't complain, but appeared to be unsound (that said the tests don't write to error types a lot).

I'd understand if you don't like the way I've done some things, I kind of expect if this is to land it would need at least a round of review to clean it up further, as some of the stuff doesn't make as much sense now.

Changes

Anyway, I've tried to make this happen a few times and it's mostly evaded me. This time I managed to get it working.

These are, broadly, the changes it makes, and why I made them, so that you have hopefully enough context to feel in-the-loop for reviewing.

  • Avoids using Box<ErrorImpl<()>> There are reasons for this:

    1. Converting between Box<T> and Box<U> where T and U don't have the same layout is UB in many (all?) cases (for example: if U is larger than T it is definitely UB for reasons mentioned in the second bit)
    2. This avoids the situations where have a not-entirely-unique Box<T>, for example when we read one out of a ManuallyDrop field, and such. ManuallyDrop doesn't save us here, since it has no impact on aliasing or validity. We could have uses MaybeUninit instead, but just using NonNull is cleaner.

    See Aliasing rules for Box<T> rust-lang/unsafe-code-guidelines#258

  • Avoids &ErrorImpl<()>, &mut ErrorImpl<()> essentially everywhere. There are too many ways this can go wrong. The most notable one that anyhow hits is that it likes to turn &ErrorImpl<()> into &ErrorImpl<E>, where E is bigger than ().

    Currently this is UB. The mental model I use for this is that the first borrows is only for one specific range of bytes, and so you don't get to decide that you're borrowing more. (It kinda sucks and applies to Box too)

    See Storing an object as &Header, but reading the data past the end of the header rust-lang/unsafe-code-guidelines#256

    The other reason for this change is that in general the code seemed to play it pretty fast and lose with pointers, which can be fine, but (among other things) sorta requires they be raw.

    That said, I was a bit more paranoid than perhaps I need to be here, both because the kind of code that would cause issues (mutable access) isn't well covered by tests, and because it's easier to just follow a rule of "always avoid &ErrorImpl<()>" rather than a lot of thinking about it a ton.

  • Avoids cases where we go &T => *const T => *mut T => &mut T.

    Annoyingly, it turns out it's not 100% true that const/mut are equivalent. Pointers that start out as const are illegal to ever write to. The biggest change was that a distinct downcast_mut function was needed in the vtable, but i think there were smaller onces.

    See Differences between *const T and *mut T. Initially *const T pointers are forever read-only? rust-lang/unsafe-code-guidelines#257

That's probably it.

Note: I also switched things away from using transumute in favor of raw ptr conversions, but in retrospect the transmute would have definitely been fine, even between NonNull/Box, so if you prefer them I can try and undo that part.

Options for cleaning this all up

So, again, I'm unsurprised if you'd rather not take it as is. It makes a lot more code unsafe, and harder to work with.

Sadly, using references is a bit of a double-edged sword. They make maintenance easier, but also make it much easier to have unsound code, since they let the compiler assume so much about what's allowed. And instead, using pointers greatly increases how much unsafe code there is (any function taking a pointer it reads from needs to be).

One option to improve things if you'd like would be add a transparent wrapper for NonNull<ErrorImpl<()>> that would have more ref-like semantics, hold a lifetime, and allow use from safe code (it would just be unsafe to create). (Then, stuff like ErrorImpl::{display,debug,error,backtrace,error_mut,etc} could be safe again, and it could be used in vtable functions to make some ref/mut versions more obviously distinct).

I didn't do this for a few reasons:

  • adds complexity, you'd have to sort out which bits are required and which are are just to make things nicer.
  • you might have a different design you'd prefer to clean this up
  • you might not want to take the patch at all (or might want to take it as is)

Oh, other big missing thing is that I need to update safety annotations on several comments, but I figured I'd do a temp check on this first.


Anyway, for clarity: I don't think any of these issues can cause any problems in practice under current compilers. It's possible that for several of them the outcome will be "stacked borrows is wrong".

That said, it feels plausible that at least some of these are real problems, and is nice if code that uses anyhow can use miri too, even if they happen to test a case that triggers an error.

P.S. very sorry for writing an whole dang novel about this PR. It's complex and didn't want to just drop it with no context, since I know how that can be.

Copy link
Owner

@dtolnay dtolnay left a comment

Choose a reason for hiding this comment

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

Thanks for the PR. My view is "stacked borrows is wrong" and needs to be adapted to accommodate what some of this code was doing, especially rust-lang/unsafe-code-guidelines#256, but I understand it will be a long time as this plays out. I am hesitant about accepting this PR because:

  • it makes things less safe, harder to work with and keep working, as you observed.
  • it increases bloat, with the duplication of vtable methods with functionally identical behavior. I really hope that an alternative will emerge for this and the vtables can be shrunk back down.
  • people may perceive it as implying that I endorse Miri's current stacked borrows-based implementation as authoritative of Rust semantics.

However Miri support in downstream code is compelling from a feature perspective so on balance I think it's worth accepting the change. Implementation-wise this seems fine to me. Feel free to send followup cleanups in subsequent PRs. Thanks!

@dtolnay dtolnay merged commit ae372df into dtolnay:master Jan 10, 2021
@thomcc
Copy link
Contributor Author

thomcc commented Jan 10, 2021

Great! This means (among other things), I can use anyhow in a custom test runtime/driver I'm working on, since it being unable to use miri for the tests it's running would be pretty unfortunate! (That said, I've already removed it, which is perhaps for the best in this case...).

I'm a little surprised, after writing the PR I saw #62 (comment), and prepared for an r-. Glad you took it!

My view is "stacked borrows is wrong" and needs to be adapted to accommodate what some of this code was doing, especially rust-lang/unsafe-code-guidelines#256

Yeah, I definitely agree... Rust has made a lot of decisions in the past to make unsafe code easier to write (no strict aliasing, no move ctors, etc), and I'm worried about going in the direction of making unsafe harder to write, in exchange for optimizations and simpler formal semantics (for example, we already lost uninitialized integers, even if they are never read from...).

... duplication of vtable methods with functionally identical behavior. I really hope that an alternative will emerge for this and the vtables can be shrunk back down.

So, I think raw refs will allow unifying these, but I believe the rules about going &T -> *const T -> *mut T -> &mut T aren't just Miri/SB being picky, and are because the LLVM IR we currently generate assumes it can't happen.

That is, as soon as you have a &T, it gets tagged as readonly in the LLVM IR, which can then be applied to all pointers based on it (not recursively, but I think it wouldn't need to be recursive in this case). That said, it's pretty likely that LLVM can't devirtualize this stuff in real world code, and it would need to.

(Ironically, you could fix LLVM's UB here and have only one downcast function by having it always takes/work with &mut T, transmuting &T -> &mut T when calling it in the normal downcast. This would definitely be Rust-level UB tho, but AFAIK not in a way reflected in the LLVM-IR, which considers &mut T to be strictly less restrictive than &T. I'm not actually suggesting this, though!)

Implementation-wise this seems fine to me. Feel free to send followup cleanups in subsequent PRs. Thanks!

If you're fine with it as-is, then that's fine. I may later try to clean up the comments (which likely are not complete in terms of "Safety:" notices, and pull unsafe into callers in things like the fmt stuff, but not sure when I'll get around to it.


P.S. At the risk of stating the obvious: if you feel strongly about Miri/SB semantics being wrong, it's probably worth participating in some extent in the UCG discussions.

That is, I think without people saying "no, unsafe code needs this" the current model won't improve (if anything it feels like it's getting stricter), and your voice would carry a lot of weight — definitely more than comes from just citing that one of your libraries does something (since the assumption is that it just needs to be fixed).

I know you're busy tho, but yeah, just thought I'd say something.

bors bot added a commit to comit-network/comit-rs that referenced this pull request Mar 17, 2021
3540: Bump anyhow from 1.0.37 to 1.0.38 r=mergify[bot] a=dependabot[bot]

Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.37 to 1.0.38.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/dtolnay/anyhow/releases">anyhow's releases</a>.</em></p>
<blockquote>
<h2>1.0.38</h2>
<ul>
<li>Support using anyhow::Error in code executed by Miri (<a href="https://github.com/dtolnay/anyhow/issues/134">#134</a>, thanks <a href="https://github.com/thomcc"><code>@​thomcc</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/dtolnay/anyhow/commit/6a16413b656f20480dc8edfa2efe01bad2b0e710"><code>6a16413</code></a> Release 1.0.38</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/f3fe8bdd043cab67fa6551d9d213376e97409fba"><code>f3fe8bd</code></a> Restore compatibility with rustc pre-1.38</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/62673e2ccf8f0b20519c3a610f8d8b5aaf99e6f9"><code>62673e2</code></a> Adopt some NonNull wrappers</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/d1b0c9a17c6108bdedab1153c48acc42b82c2649"><code>d1b0c9a</code></a> Only need vtable function for a type erased ErrorImpl</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/bd5e39bd15ad7af45d2d58e6623ef9ac0c774c01"><code>bd5e39b</code></a> Elide ErrorImpl type parameter if erased</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/ae372df93ff821347f2183f546b167c9bcd37358"><code>ae372df</code></a> Merge pull request 134 from thomcc/mirimiri</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/52c2c4c4424e2f339c1b98796400a89c680ae948"><code>52c2c4c</code></a> Fix parse failure in doc test</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/9757e7f6ac862a3d7f241e3fb070bf192ffb1c97"><code>9757e7f</code></a> Add miri to CI</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/87fdd9ec91b40f08f975cf3cb6981c2037d5f458"><code>87fdd9e</code></a> Allow running tests under miri</li>
<li><a href="https://github.com/dtolnay/anyhow/commit/cb841d6e3293daf6f2f7f75cd618d347e1dd5c41"><code>cb841d6</code></a> Fix catching clippy warnings as CI failures</li>
<li>Additional commits viewable in <a href="https://github.com/dtolnay/anyhow/compare/1.0.37...1.0.38">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=anyhow&package-manager=cargo&previous-version=1.0.37&new-version=1.0.38)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually


</details>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Repository owner locked and limited conversation to collaborators Oct 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants