Fix RUSTSEC-2026-0197: swap_columns/swap_elements trigger UB when indices alias#566
Open
haraldreingruber-dedalus wants to merge 1 commit into
Open
Conversation
718d899 to
dcf3da7
Compare
…ces alias
When swap_columns is called with a == b, the previous implementation created
two mutable references (&mut self[a] and &mut self[b]) to the same memory
location (aliased &mut UB). Additionally, even for distinct indices a != b,
using ptr::swap(&mut self[a], &mut self[b]) is UB under Miri (Stacked
Borrows): the second IndexMut call creates a new unique reborrow of all of
self, invalidating the tag from the first call.
The same class of bug existed in:
- Array::swap_elements (structure.rs)
- Matrix{2,3,4}::swap_columns
- Matrix{2,3,4}::swap_elements
Fix: since S: BaseFloat implies S: Copy, column vectors and scalar elements
are Copy. We can swap entirely without unsafe by using a temporary variable:
let tmp = self[a];
self[a] = self[b];
self[b] = tmp;
This avoids creating multiple mutable references simultaneously and requires
no unsafe code at all. Verified clean under Miri.
Tests added in tests/matrix.rs covering all three matrix sizes for:
- swap_columns with equal indices (no-op, was UB)
- swap_columns with distinct indices (correct swap)
- swap_elements with same position (no-op, was UB)
- swap_elements with same column, different rows
Note: the tests verify correctness but do not catch the soundness bug in a
regular cargo test run. Use cargo +nightly miri test to detect aliasing UB.
Fixes: rustgd#565
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
dcf3da7 to
093f70a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #565.
Problem
Matrix2::swap_columns,Matrix3::swap_columns,Matrix4::swap_columns, and the correspondingswap_elementsimplementations all used:When
a == b, this creates two mutable references (&mut self[a]and&mut self[b]) to the same memory location. This violates Rust's aliasing guarantees and constitutes undefined behaviour under Miri (Stacked Borrows), even though the call is made from safe Rust.Additionally, even for distinct indices,
ptr::swap(&mut self[a], &mut self[b])is flagged by Miri: both calls go throughIndexMut::index_mut(&mut self, …), so the second call creates a new unique reborrow of all ofself, invalidating the tag from the first call — a Stacked Borrows violation even though the pointed-to bytes don't overlap.The same class of bug existed in:
Array::swap_elementsinstructure.rs(UB wheni == j)Matrix{2,3,4}::swap_elements(UB when both indices share the same column, i.e.ac == bc), because two exclusive borrows of the same column vector are created simultaneouslyFix (this branch)
Since
S: BaseFloatimpliesS: Copy, column vectors and scalar elements areCopy. Allunsafeis removed and the swap is done via a temporary variable:This is fully clean under Miri. An alternative, minimal fix using early-returns only (keeping
ptr::swapfor distinct indices) is available on branchfix-swap-columns-early-return— that branch resolves the originally reported aliasing UB but is still flagged by Miri (Stacked Borrows) for the distinct-index path.Tests
Added tests in
tests/matrix.rscovering all three matrix sizes for:swap_columnswith equal indices (was UB, now no-op)swap_columnswith distinct indices (correct swap still works)swap_elementswith the same position (was UB, now no-op)swap_elementswith the same column and different rows (was potentially UB, now correct)Note
These tests verify correctness (the operations produce the right values) but they do not catch the soundness bug in a regular
cargo testrun. Becauseptr::swapof a location with itself happens to leave the value unchanged, the value-equality assertions pass with or without the fix. The bug can only be reliably detected by running the tests under Miri (cargo +nightly miri test).Feel free to remove the new tests if they don't seem valuable enough without Miri coverage in CI.
Note
This fix was developed with the assistance of GitHub Copilot. While investigating the reported
swap_columnsUB, similar aliasing issues were also found and fixed inArray::swap_elementsandMatrix*::swap_elements(same-column case), which were not explicitly called out in the original issue.