-
Notifications
You must be signed in to change notification settings - Fork 349
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
TB: Missed UB in the "optimization" that allows skipping subtrees #3846
Comments
The question of how to resolve this is interesting. One option is to remove this optimization, but it surely exists for a reason. Another option is to weaken it, by making retags reset this "already experienced a foreign read" marker on all their parents across the entire range. But this introduces extra overhead, and it should be tested what the performance impact is here. Luckily, not all is lost. Instead on optimizing traversals based on "what was the last access", one can instead optimize based on permissions -- it is sound to skip foreign reads when a node is Someone can have lots of fun profiling this. 🙃 |
Wow, good catch! Did we not have exhaustive tests checking this optimization, or were they not exhaustive enough? The immediate next step should be to disable that optimization, and then we can take time to figure out another, actually sound optimization. |
There was an exhaustive test checking that foreign reads after foreign writes are no-ops, and another one that two foreign accesses repeated are no-ops. These were not the issue here. The problem was that this option was simply missed. The comment on that function prominently states:
But there's the secret fourth option: silently add a new node with lazy permission, but without ever causing an access at the position where the node is lazy (outside of the initial range). But that's of course not an action that changes any node, so it was overlooked in the exhaustive tests (or rather: what would the exhaustive test do there?). The problem with them was that they only checked things for one node, but of course the problem here is we add a second node. The implicitly assumed tree invariant was that "if the foreign last access to a node was Looking back, these lazy permissions that are added silently are a pretty large footgun. #3732 was similarly due to them being overlooked. |
See rust-lang#3846 for more information.
This commit supplies a real fix, which makes retags more complicated, at the benefit of making accesses more performant.
Tree Borrows has an optimization where we skip subtree updates if we know that the subtree has not changed -- at least that was the idea. More specifically, every node records if its last access was a child access, a foreign read, or a foreign read/write (mixed, since a write followed by a read is idempotent). Then, if e.g. the last access was a foreign read, and the next access is also a foreign read, we should be able to skip the entire subtree, since it should already be idempotent.
But turns out, we can not always skip the tree. The reason is that lazy nodes exist, which are "silently" created across the entire allocation whenever you do a reborrow, while the reborrow only emits an event for the actually accessed range. Combined with protected
Reserved
being supposed to become conflicted on a foreign read (even when lazy), this leads to missed UB. The optimization for foreign writes can also be broken by aReservedIM
node, which would shield its children from foreign write accesses, which is even worse.Here is an example:
Because the
foo
reference is internally marked as "already experienced a foreign read," the second foreign read indo_something
skips the entire subtree, and thus does not mark that the lazy part ofx
as conflicted. Thus, the later read is possible, whereas it should not be.CC @Vanille-N
I was not able to turn this into a LLVM misoptimization, but the generated LLVM code has UB, I believe (accesses a
noalias
ptr and another pointer pointing to the same memory location). In general, turning this into a miscompilation is hard since LLVM does not seem to be willing to reorder reads and writes that are the first access.The text was updated successfully, but these errors were encountered: