JumpThreading miscompiles union field reads after sibling writes, breaking safe Rust memory safety (missing aliasing flood in value_analysis)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Summary
rustc_mir_dataflow::value_analysis::Map::for_each_aliasing_place invalidates enum Variant/Discriminant siblings on a write, but does not invalidate Field siblings when the parent is a union. Because union fields alias the same storage, a write to U.b must flood the tracked state of U.a. It doesn't, so JumpThreading (default in -C opt-level=1+) and DataflowConstProp (-Zmir-opt-level=3) consume stale values and miscompile.
Security / soundness implication: This is a violation of Rust's core memory-safety contract. Miri confirms the source program is well-defined, yet the optimized binary executes invalid memory accesses (I was able to POC: controlled heap OOB reads, cross-allocation corruption, and function-pointer hijacking in synthetic tests). The developer need not write unsafe in the application crate a Miri-clean library encapsulating a union tag is sufficient.
Regression:
- Clean: 1.80.0, 1.80.1
- Affected: 1.81.0 → 1.98.0 stable, 1.100.0-nightly
Minimal reproducer:
union U { a: u8, b: u8 }
#[inline(never)]
fn f(x: u8) -> u8 {
let mut u = U { a: x };
let r = unsafe { match u.a { 0 => 100, _ => 200 } };
u.b = 5; // overwrites the same byte as u.a
// BUG: JumpThreading threads this to the `0` arm using the stale pre-write value
let r2 = unsafe { match u.a { 0 => 1, _ => 2 } };
r + r2
}
fn main() {
assert_eq!(f(0), 102); // fails in release: gets 101
assert_eq!(f(7), 202); // ok
}
Results:
- Build -C opt-level=0 -> f(0) 102
- Build -C opt-level=2 -> f(0) 101
- Build -C opt-level=2 -Zmir-enable-passes=-JumpThreading -> f(0) 102
- Build cargo miri run -> f(0) 102
Root cause:
for_each_aliasing_place (compiler/rustc_mir_dataflow/src/value_analysis.rs, stable 1.98.0 ) has for_each_variant_sibling for enums but no equivalent for union Field siblings:
if let TrackElem::Variant(..) | TrackElem::Discriminant = elem {
self.for_each_variant_sibling(index, sub, f);
}
// Missing: if TrackElem::Field(..) && parent_is_union { flood siblings }
Additional note:
A transferred shape also miscompiles: facts cross siblings via copy assignments (match u.a → u.b = 5 → u.c = u.a → match u.c), because process_copy in the backward pass renames the active condition from c onto a, and the write to b does not flood a.
Miri confirms the source program is well-defined; the defect is purely in the optimizer.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in compiler/rustc_mir_dataflow/src/value_analysis.rs, focusing on Map::for_each_aliasing_place and its existing enum sibling handling. Reproduce the minimal example at opt-level=0 and opt-level=2, then compare with JumpThreading disabled and under Miri. Done means union field writes no longer leave stale sibling values for JumpThreading or DataflowConstProp, including the transferred-shape case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100