Skip to content

JumpThreading miscompiles union field reads after sibling writes, breaking safe Rust memory safety (missing aliasing flood in value_analysis) #161898

Description

@Madan301

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

A-mir-optArea: MIR optimizationsC-bugCategory: This is a bug.I-miscompileIssue: Correct Rust code lowers to incorrect machine codeP-highHigh priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.WG-mir-optWorking group: MIR optimizationsregression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions