You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Surprising behavior of aliasing model: references that are transmuted into existence might not have the expected properties #593
This code does not have UB under Stacked Borrows and Tree Borrows:
fnmain(){letmut data = 1;let x = &mut data;letmut y = &mut2;unsafe{ std::ptr::copy_nonoverlapping(&rawconst x,&rawmut y,1);}*x = 3;*y = 4;*x = 5;*y = 6;}
This is expected behavior. The copy_nonoverlapping makes y a perfect copy of x, those two just contain the same pointer. Mutable references are unique in the sense that no "other" pointer is allowed to be used while they are live -- but here, there's no "other" pointer, it's twice the same pointer.
Within the framework that SB and TB are built in, I don't see any way to avoid that. I don't think *x = 3 should mutate x itself, so it follows that x and y just are and remain the same pointer.
I am filing this issue mostly to remind us that when we move towards making decisions around the aliasing model, we should explicitly discuss this case. But maybe someone has an idea for how a model could make this UB?
changed the title [-]Surprising behavior of aliasing model: raw pointers that are transmuted into existence might not be "unique"[/-][+]Surprising behavior of aliasing model: mutable references that are transmuted into existence might not be "unique"[/+]on Dec 13, 2025
I think it's OK for dereferencing a unique reference to mutate the reference itself (which would of course allow solving the specific example you gave by causing a dereference of a unique reference to do a degenerate typed copy of the reference into itself). After all, DerefMut takes &mut self and you can't write through an & &mut T, which are both outcomes that you would expect if writing through a reference mutated the reference. Treating a dereference of a unique reference as a mutation of the reference itself leads to interesting consequences – while looking at the consequences, they started to form an aliasing model based primarily around the idea of "doing anything with a mutable reference changes which provenance is required to access the memory holding its target, and changes the provenance of the reference itself to match" – this gives you most of a Rust aliasing model for free (e.g. writing to memory will break the provenance of any shared references to that memory, causing future uses of them to become undefined behavior).
However, while working on it, I found another case of effectively the same phenomenon which is harder to fix: shared references that are transmuted into existence might not freeze their targets (this does not have UB in Stacked Borrows or Tree Borrows):
use core::cell::Cell;use core::ptr;fnmain(){let a = Cell::new(4i32);let b = &a;letmut c:&i32 = &0;unsafe{ ptr::copy(&rawconst b as*const&i32,&rawmut c,1);}dbg!(c);// 4
a.set(5);dbg!(c);// 5}
This one can't be fixed using a "mutating through a reference also mutates the reference" principle because &Cell<i32> is Copy and mutating through it wouldn't be expected to mutate its copies, so you can't have changes through it break the provenance of copies of it like you can with mutable references.
At least for that specific example, we could get the optimization if we say that the validity invariant of an &i32 reference includes not only dereferenceability but also the requirement of this reference being in the "frozen" state (in TB terms).
Would that be compatible with transmuting raw pointers that arrived via FFI into references (and would it be a problem if it weren't)? I don't know how often people transmute the pointers rather than borrowing them (but there is a guarantee that they have the same representation, which might encourage people to do it as a transmute).
If the FFI does not use cross-lang LTO, you can always pretend that provenance got updated as appropriate during the FFI transition. With cross-lang LTO however, indeed such transmutes could become problematic. That's a pretty fundamental tradeoff -- either we allow code to create shared references that are not frozen, or not even FFI can do that.
changed the title [-]Surprising behavior of aliasing model: mutable references that are transmuted into existence might not be "unique"[/-][+]Surprising behavior of aliasing model: references that are transmuted into existence might not have the expected properties[/+]on Apr 27, 2026
The shared reference version of this might be something GVN already relies on? Not sure. Cc @dianqk@cjgillot
(EDIT: Yeah looks like it does.)
We should probably experiment with requiring shared references to be frozen. However that will make mutable-to-shared-ref transmutes UB, which could itself be a source of problems. (It's also not an entirely trivial check since the TB state is per-location and obviously it only needs to be frozen outside UnsafeCell.)
OTOH if we do this then we don't need to necessarily retag everything everywhere to do things like this.
I was referring to the example in this comment which reads 4 from c (which is a non-cell shared reference of type &i32) and later reads 5 from c – my suggestion was to make this UB because it reads (via a shared reference) memory that had changed since the shared reference's provenance was created.
There might be a misunderstanding here: when I say "read via a shared reference", the "shared reference" is referring to the type which performs the read in surface Rust, not to the type of reference that might be indicated by any tags that exist (for the address in question) on the reference's provenance. Tree Borrows disallows the latter, but not the former; I am suggesting that the former should be disallowed as well.
I was referring to the example in this comment which reads 4 from c (which is a non-cell shared reference of type &i32) and later reads 5 from c – my suggestion was to make this UB because it reads (via a shared reference) memory that had changed since the shared reference's provenance was created.
There might be a misunderstanding here: when I say "read via a shared reference", the "shared reference" is referring to the type which performs the read in surface Rust, not to the type of reference that might be indicated by any tags that exist (for the address in question) on the reference's provenance. Tree Borrows disallows the latter, but not the former; I am suggesting that the former should be disallowed as well.
Yeah that's what this issue is about -- how we could make the program UB. We agree that it should be disallowed; so far there is no proposal for how it could be disallowed.
Or are you saying that your proposal should be read as an operational semantics? It is not well-defined. That's what I pointed out to you above: Your proposal uses the phrase "since the shared reference's provenance was created" while there isn't a shared reference being created here. (Potentially we could consider the *x load to construct a shared reference as it's doing a typed &i32 load, but that shared reference is then only used once, so the mutation condition you gave is not violated by the program). So it's not meaningful to ask about when it's provenance was created or so.
If you think there's an operational semantics here, please write it out in operational terms. Otherwise 3 people reading this will have 5 different ideas of what you actually mean.
I intended it as an operational semantics, but for the purpose of resolving the second example in this thread (the one in this post), not the example in the OP. (It does not fix the example in the OP, but the two examples likely need separate fixes.) The example in question does not contain x, so I think we were talking about different programs (which likely explains the confusion). Repeating the example to avoid confusion:
use core::cell::Cell;use core::ptr;fnmain(){let a = Cell::new(4i32);let b = &a;letmut c:&i32 = &0;unsafe{ ptr::copy(&rawconst b as*const&i32,&rawmut c,1);}dbg!(c);// 4
a.set(5);dbg!(c);// 5}
In this situation, c is (in the source program) a shared reference &i32, and yet it's possible under Shared Borrows or Tree Borrows to read two different values from it without causing any aliasing model violations.
My opsem proposal was that attempts to use c as though it were a shared reference should be UB if the memory being read has been changed since its provenance was created. In this case, the second dbg!(c) dereferences c, but the memory being dereferenced has changed since its provenance was created (the provenance in question was created by the borrow in the let b = &a; line), so this rule would consider the second dbg! to be undefined behavior (whereas it is defined behavior in the current versions of Shared Borrows and Tree Borrows).
To define "use X as though it were a shared reference" precisely, even in the presence of transmutes and outside-subobject reads, I would define it as "any read through a reborrow or transitive reborrow of X in which X's type at the time when the relevant reborrow of X occurred was a shared reference type, or any read directly through X in which X's type is a shared reference type" (adjusted for cells in a way that's consistent with how cells are handled in the rest of the aliasing model, and defining "read" to include retag reads in cases where the aliasing model would perform a retag read).
I intended it as an operational semantics, but for the purpose of resolving the second example in this thread (the one in #593 (comment)), not the example in the OP.
Ah I see.
That example similarly doesn't construct an &i32 reference though, it constructs an &Cell<i32> reference and then stores it in memory in a location that we later load at type &i32. That load, the *c hidden inside the dbg!, is the first time an &i32 gets crated (ignoring the dummy &0), and it is very short-lived.
Or are you saying that even &Cell<i32> references should track foreign writes somehow? That on its own already may be problematic, the entire point of those references is that they don't care about foreign accesses. But even then it seems you are saying that when we load a load through a pointer, the type of that pointer matters. That's a highly non-trivial departure from SB/TB, and I am not convinced it'll work very well. Loads occur from places, not pointers, so one would have to define how this kind of information propagates through place evaluation. Currently, the tag (and the pointee type) is the only information we preserve.
Also your proposal fails with this variant I think:
use core::cell::Cell;use core::ptr;fnmain(){let a = Cell::new(4i32);let b = &rawconst a;// no new provenance being created!letmut c:&i32 = &0;unsafe{ ptr::copy(&rawconst b as*const*consti32as*const&i32,&rawmut c,1);}dbg!(c);// 4
a.set(5);dbg!(c);// 5}
The idea was that when you dereference a reference, the type of that reference determines what can be done with the resulting place (i.e. the aliasing information of the place remembers that it was produced by dereferencing a reference at shared reference type, so subsequent attempts to read the value at that place can be UB if the memory was written since the reference's provenance was created).
Your variant example made me realise that this idea is not compatible with Tree Borrows, though: although it considers the behavior of the example in question to be undefined, it would still consider it undefined if the write to the cell happened before the raw pointer was created, which would be very surprising and is probably undesirable. (This happens because, in Tree Borrows, the creation of the raw pointer doesn't create a new provenance, so the relevant provenance is that of the place that stores the local variable `a`, and the cell has been written since that provenance was created at the start of the function call).
Your variant example made me realise that this idea is not compatible with Tree Borrows, though: although it considers the behavior of the example in question to be undefined,
You mean, with your proposal (that I haven't really grasped yet) added? In TB today the code is accepted.
in Tree Borrows, the creation of the raw pointer doesn't create a new provenance,
Indeed, that's the point of the variant. In SB, raw pointers create a new provenance, and I think that was one of its biggest flaws. Having to figure out when raw pointers get created from references is very finicky, and I feel quite strongly that this is a question we should simply not have to answer.
Right, I meant the example is undefined under my proposal.
I think there's a contradiction between desirable features here. Here's a slightly modified version of your variant (which also demonstrates that even if raw pointers didn't share the provenance of the place they were created from, it wouldn't solve the problem):
use core::cell::Cell;use core::ptr;fnmain(){let a = Cell::new(4i32);let b = &rawconst a;unsafe{(*b).set(5);}letmut c:&i32 = &0;unsafe{ ptr::copy(&rawconst b as*const&i32,&rawmut c,1);}dbg!(c);unsafe{(*b).set(6);}unsafe{(*b).set(7);}iffalse{unsafe{ ptr::copy(&rawconst b as*const&i32,&rawmut c,1);}}dbg!(c);unsafe{(*b).set(8);}}
As written, the program reads two different values from c (which is &i32). I consider it undesirable for the aliasing model to consider this code to have defined behavior (because it means that the aliasing model cannot assume that the target of a shared reference will remain immutable unless it can see how the shared reference was created, which in turn prevents a wide range of useful optimizations: notably, async blocks take their own local variables by reference, and thus are unable to see how their own local variables were created).
Meanwhile, if you replace the if false with if true you obtain a program which does the sequence "write to a cell, transmute-without-retag a pointer to that cell into a shared reference and read through it, then write to the cell again" twice. If the program had created the shared reference by borrowing a dereference of the pointer (rather than transmuting it), the behavior would unambiguously be defined (e.g. this sort of pattern is possible using RefCell). Declaring the if true version of this code to have undefined behavior would therefore effectively be stating that pointer-to-reference transmutes do not produce a usable reference, as that's the only difference between this program and an unambiguously legal one.
The problem is that these two programs are almost identical: the only difference is whether or not to perform an apparently redundant untyped copy of b into c, so in order for the if false version of the program to have undefined behavior and the if true version to have defined behavior, something has to change the provenance of b or c after the first ptr::copy call and before the second (otherwise the second copy will be a no-op). c is not written to or through during the time period in question, and b is a raw pointer, so it is hard to see how either provenance could change.
I think "pointer-to-reference transmutes might not produce a usable reference" is the most reasonable possibility here, but all of them look fairly terrible.
I think "pointer-to-reference transmutes might not produce a usable reference" is the most reasonable possibility here, but all of them look fairly terrible.
That is what I suggested above, right? We say that it's UB to create a shared reference with a non-frozen provenance.
I think the next step is to implement that in Miri so we can check how commonly code violates this requirement.
Right; I think the only room for choice with this approach is at what specific time the UB happens. Should the following be UB, or would you have to use c more than this minimally in order for the code to be undefined?:
use core::ptr;fnmain(){let a = 1;let b = &rawconst a;letmut c:&i32 = &0;unsafe{ ptr::copy(&rawconst b as*const&i32,&rawmut c,1);}let _ = *c;}
I suspect that this should probably not be UB in order to be consistent with the opsem for closure captures, but the problem with considering this to be not-UB is that you would then need to track that the place *c had been created from an incorrectly tagged shared reference. I guess the easiest implementation would be to say that places that are created from an incorrectly tagged shared reference don't have a usable provenance.
I would start by making such code UB too. This is after all a typed load of c and thus all its validity requirements apply, including the new one about its provenance.
If there's evidence that this causes unnecessary UB in the wild we can reconsider.
I'd really like to avoid "&mut T → &T transmute is UB", I think that would be a much too heavy hammer.
Here's a sketch of a possible semantics:
The tag of every reference and local variable has an associated "subtag map" (of type tag → tag), which starts out empty. Whenever a load is performed through some reference or local r, each tag associated with the bytes of the loaded value $val$1 is looked up in the map. Each tag $T$ in $val$ which corresponds to a key in the r's subtag map is replaced with the associated value for that key. If there is no entry for $T$ in the map, one is created by retagging the original tag from val. The resulting value of the load, $val\prime$, has all its tags replaced via this process.
Concretely:
fnmain(){letmut data = 1;let x = &mut data;// Tag associated with local variable `x`// (let's call it `xloc_tag`)// initialized with empty subtag map.// Tag associated with the reference stored in variable `x`// (let's call it `xref_tag`)// also initialized with empty subtag map.letmut y = &mut2;// Tag associated with local variable `y`// (let's call it `yloc_tag`)// initialized with empty subtag map.// Tag associated with the reference stored in variable `y`// (let's call it `yref_tag`)// also initialized with empty subtag map.unsafe{ std::ptr::copy_nonoverlapping(&rawconst x,&rawmut y,1);}// Now the reference stored in variable `y` is a copy of the one in `x`,// so it has tag `xref_tag`.*x = 3;// The load of local variable `x` causes an entry to be inserted into `xloc_tag`'s subtag map,// `xref_tag` => `xreftag_xloc`, the latter being retagged from the former.// `xreftag_xloc` is used as the tag of the mutable reference resulting from the load.*y = 4;// The load of local variable `y` causes an entry to be inserted into `yloc_tag`'s subtag map,// `xref_tag` => `xreftag_yloc`, the latter being retagged from the former.// `xreftag_yloc` is used as the tag of the mutable reference resulting from the load.// Assignment via that reference is therefore a foreign write for `xreftag_xloc`,// disabling that tag.*x = 5;// The load of local variable `x` engenders a lookup of `xref_tag` in `xloc_tag`'s subtag map.// This returns `xreftag_xloc`, which is therefore used as the tag of the mutable reference resulting from the load.// However, this tag is disabled, so we have created a disabled mutable reference, which is UB!*y = 6;}
(There are a lot of holes left to fill before this can become a real proposal, of course. Like "what happens to the subtag map when you reborrow a reference")
Footnotes
Exempting anything inside MaybeDangling, MaybeUninit, etc. ↩
what happens to the subtag map when you reborrow a reference
Thinking about this more: whenever a subtag map lookup fails, we should walk the chain of ancestor tags, check each of their subtag maps, retag the first result found, and insert that into the child map. Only if nothing is found should the original tag from the loaded value be retagged.
Activity
[-]Surprising behavior of aliasing model: raw pointers that are transmuted into existence might not be "unique"[/-][+]Surprising behavior of aliasing model: mutable references that are transmuted into existence might not be "unique"[/+]ais523 commentedon Feb 6, 2026
I think it's OK for dereferencing a unique reference to mutate the reference itself (which would of course allow solving the specific example you gave by causing a dereference of a unique reference to do a degenerate typed copy of the reference into itself). After all,
DerefMuttakes&mut selfand you can't write through an& &mut T, which are both outcomes that you would expect if writing through a reference mutated the reference. Treating a dereference of a unique reference as a mutation of the reference itself leads to interesting consequences – while looking at the consequences, they started to form an aliasing model based primarily around the idea of "doing anything with a mutable reference changes which provenance is required to access the memory holding its target, and changes the provenance of the reference itself to match" – this gives you most of a Rust aliasing model for free (e.g. writing to memory will break the provenance of any shared references to that memory, causing future uses of them to become undefined behavior).However, while working on it, I found another case of effectively the same phenomenon which is harder to fix: shared references that are transmuted into existence might not freeze their targets (this does not have UB in Stacked Borrows or Tree Borrows):
This one can't be fixed using a "mutating through a reference also mutates the reference" principle because
&Cell<i32>isCopyand mutating through it wouldn't be expected to mutate its copies, so you can't have changes through it break the provenance of copies of it like you can with mutable references.theemathas commentedon Feb 6, 2026
As a corollary of the above, this means that the assertion in the below code cannot be optimized out. This seems very unfortunate.
Here's the full code where the assertion fails without UB occurring.
RalfJung commentedon Feb 6, 2026
At least for that specific example, we could get the optimization if we say that the validity invariant of an
&i32reference includes not only dereferenceability but also the requirement of this reference being in the "frozen" state (in TB terms).ais523 commentedon Feb 6, 2026
Would that be compatible with transmuting raw pointers that arrived via FFI into references (and would it be a problem if it weren't)? I don't know how often people transmute the pointers rather than borrowing them (but there is a guarantee that they have the same representation, which might encourage people to do it as a transmute).
RalfJung commentedon Feb 6, 2026
If the FFI does not use cross-lang LTO, you can always pretend that provenance got updated as appropriate during the FFI transition. With cross-lang LTO however, indeed such transmutes could become problematic. That's a pretty fundamental tradeoff -- either we allow code to create shared references that are not frozen, or not even FFI can do that.
[-]Surprising behavior of aliasing model: mutable references that are transmuted into existence might not be "unique"[/-][+]Surprising behavior of aliasing model: references that are transmuted into existence might not have the expected properties[/+]RalfJung commentedon Apr 27, 2026
The shared reference version of this might be something GVN already relies on? Not sure. Cc @dianqk @cjgillot
(EDIT: Yeah looks like it does.)
We should probably experiment with requiring shared references to be frozen. However that will make mutable-to-shared-ref transmutes UB, which could itself be a source of problems. (It's also not an entirely trivial check since the TB state is per-location and obviously it only needs to be frozen outside
UnsafeCell.)OTOH if we do this then we don't need to necessarily retag everything everywhere to do things like this.
Auto merge of #154341 - RalfJung:retag-on-typed-copy, r=oli-obk
Auto merge of #154341 - RalfJung:retag-on-typed-copy, r=oli-obk
Auto merge of #154341 - RalfJung:retag-on-typed-copy, r=oli-obk
Auto merge of #154341 - RalfJung:retag-on-typed-copy, r=oli-obk
12 remaining items
ais523 commentedon Aug 29, 2026
I was referring to the example in this comment which reads 4 from
c(which is a non-cell shared reference of type&i32) and later reads 5 fromc– my suggestion was to make this UB because it reads (via a shared reference) memory that had changed since the shared reference's provenance was created.There might be a misunderstanding here: when I say "read via a shared reference", the "shared reference" is referring to the type which performs the read in surface Rust, not to the type of reference that might be indicated by any tags that exist (for the address in question) on the reference's provenance. Tree Borrows disallows the latter, but not the former; I am suggesting that the former should be disallowed as well.
Jules-Bertholet commentedon Aug 29, 2026
Reserved 🤦 – yes, I suppose it would have to be a special magic variant of a fake read. Which is of course less appealing…
RalfJung commentedon Aug 30, 2026
Yeah that's what this issue is about -- how we could make the program UB. We agree that it should be disallowed; so far there is no proposal for how it could be disallowed.
Or are you saying that your proposal should be read as an operational semantics? It is not well-defined. That's what I pointed out to you above: Your proposal uses the phrase "since the shared reference's provenance was created" while there isn't a shared reference being created here. (Potentially we could consider the
*xload to construct a shared reference as it's doing a typed&i32load, but that shared reference is then only used once, so the mutation condition you gave is not violated by the program). So it's not meaningful to ask about when it's provenance was created or so.If you think there's an operational semantics here, please write it out in operational terms. Otherwise 3 people reading this will have 5 different ideas of what you actually mean.
ais523 commentedon Aug 30, 2026
I intended it as an operational semantics, but for the purpose of resolving the second example in this thread (the one in this post), not the example in the OP. (It does not fix the example in the OP, but the two examples likely need separate fixes.) The example in question does not contain
x, so I think we were talking about different programs (which likely explains the confusion). Repeating the example to avoid confusion:In this situation,
cis (in the source program) a shared reference&i32, and yet it's possible under Shared Borrows or Tree Borrows to read two different values from it without causing any aliasing model violations.My opsem proposal was that attempts to use
cas though it were a shared reference should be UB if the memory being read has been changed since its provenance was created. In this case, the seconddbg!(c)dereferencesc, but the memory being dereferenced has changed since its provenance was created (the provenance in question was created by the borrow in thelet b = &a;line), so this rule would consider the seconddbg!to be undefined behavior (whereas it is defined behavior in the current versions of Shared Borrows and Tree Borrows).To define "use X as though it were a shared reference" precisely, even in the presence of transmutes and outside-subobject reads, I would define it as "any read through a reborrow or transitive reborrow of X in which X's type at the time when the relevant reborrow of X occurred was a shared reference type, or any read directly through X in which X's type is a shared reference type" (adjusted for cells in a way that's consistent with how cells are handled in the rest of the aliasing model, and defining "read" to include retag reads in cases where the aliasing model would perform a retag read).
RalfJung commentedon Aug 31, 2026
Ah I see.
That example similarly doesn't construct an
&i32reference though, it constructs an&Cell<i32>reference and then stores it in memory in a location that we later load at type&i32. That load, the*chidden inside thedbg!, is the first time an&i32gets crated (ignoring the dummy&0), and it is very short-lived.Or are you saying that even
&Cell<i32>references should track foreign writes somehow? That on its own already may be problematic, the entire point of those references is that they don't care about foreign accesses. But even then it seems you are saying that when we load a load through a pointer, the type of that pointer matters. That's a highly non-trivial departure from SB/TB, and I am not convinced it'll work very well. Loads occur from places, not pointers, so one would have to define how this kind of information propagates through place evaluation. Currently, the tag (and the pointee type) is the only information we preserve.Also your proposal fails with this variant I think:
ais523 commentedon Aug 31, 2026
RalfJung commentedon Aug 31, 2026
You mean, with your proposal (that I haven't really grasped yet) added? In TB today the code is accepted.
Indeed, that's the point of the variant. In SB, raw pointers create a new provenance, and I think that was one of its biggest flaws. Having to figure out when raw pointers get created from references is very finicky, and I feel quite strongly that this is a question we should simply not have to answer.
ais523 commentedon Aug 31, 2026
Right, I meant the example is undefined under my proposal.
I think there's a contradiction between desirable features here. Here's a slightly modified version of your variant (which also demonstrates that even if raw pointers didn't share the provenance of the place they were created from, it wouldn't solve the problem):
As written, the program reads two different values from
c(which is&i32). I consider it undesirable for the aliasing model to consider this code to have defined behavior (because it means that the aliasing model cannot assume that the target of a shared reference will remain immutable unless it can see how the shared reference was created, which in turn prevents a wide range of useful optimizations: notably,asyncblocks take their own local variables by reference, and thus are unable to see how their own local variables were created).Meanwhile, if you replace the
if falsewithif trueyou obtain a program which does the sequence "write to a cell, transmute-without-retag a pointer to that cell into a shared reference and read through it, then write to the cell again" twice. If the program had created the shared reference by borrowing a dereference of the pointer (rather than transmuting it), the behavior would unambiguously be defined (e.g. this sort of pattern is possible usingRefCell). Declaring theif trueversion of this code to have undefined behavior would therefore effectively be stating that pointer-to-reference transmutes do not produce a usable reference, as that's the only difference between this program and an unambiguously legal one.The problem is that these two programs are almost identical: the only difference is whether or not to perform an apparently redundant untyped copy of
bintoc, so in order for theif falseversion of the program to have undefined behavior and theif trueversion to have defined behavior, something has to change the provenance ofborcafter the firstptr::copycall and before the second (otherwise the second copy will be a no-op).cis not written to or through during the time period in question, andbis a raw pointer, so it is hard to see how either provenance could change.I think "pointer-to-reference transmutes might not produce a usable reference" is the most reasonable possibility here, but all of them look fairly terrible.
RalfJung commentedon Aug 31, 2026
That is what I suggested above, right? We say that it's UB to create a shared reference with a non-frozen provenance.
I think the next step is to implement that in Miri so we can check how commonly code violates this requirement.
ais523 commentedon Aug 31, 2026
Right; I think the only room for choice with this approach is at what specific time the UB happens. Should the following be UB, or would you have to use
cmore than this minimally in order for the code to be undefined?:I suspect that this should probably not be UB in order to be consistent with the opsem for closure captures, but the problem with considering this to be not-UB is that you would then need to track that the place
*chad been created from an incorrectly tagged shared reference. I guess the easiest implementation would be to say that places that are created from an incorrectly tagged shared reference don't have a usable provenance.RalfJung commentedon Aug 31, 2026
I would start by making such code UB too. This is after all a typed load of
cand thus all its validity requirements apply, including the new one about its provenance.If there's evidence that this causes unnecessary UB in the wild we can reconsider.
Auto merge of rust-lang#154341 - RalfJung:retag-on-typed-copy, r=oli-obk
Auto merge of #154341 - RalfJung:retag-on-typed-copy, r=oli-obk
Jules-Bertholet commentedon Oct 10, 2026
I'd really like to avoid "
&mut T→&Ttransmute is UB", I think that would be a much too heavy hammer.Here's a sketch of a possible semantics:
The tag of every reference and local variable has an associated "subtag map" (of type tag → tag), which starts out empty. Whenever a load is performed through some reference or local$val$ 1 is looked up in the map. Each tag $T$ in $val$ which corresponds to a key in the $T$ in the map, one is created by retagging the original tag from $val\prime$ , has all its tags replaced via this process.
r, each tag associated with the bytes of the loaded valuer's subtag map is replaced with the associated value for that key. If there is no entry forval. The resulting value of the load,Concretely:
(There are a lot of holes left to fill before this can become a real proposal, of course. Like "what happens to the subtag map when you reborrow a reference")
Footnotes
Exempting anything inside
MaybeDangling,MaybeUninit, etc. ↩Jules-Bertholet commentedon Oct 11, 2026
Thinking about this more: whenever a subtag map lookup fails, we should walk the chain of ancestor tags, check each of their subtag maps, retag the first result found, and insert that into the child map. Only if nothing is found should the original tag from the loaded value be retagged.