Description
The documentation for align_offset
says
If it is not possible to align the pointer, the implementation returns usize::max_value().
It does not give any details of when it might not be possible to perform the alignment. My reading of that is that the user must always be prepared for failure to happen. In accordance with that, a while ago I adjusted the docs for align_to
(the preferred way to use align_offset
) to say
This method splits the slice into three distinct slices: prefix, correctly aligned middle slice of a new type, and the suffix slice. The method does a best effort to make the middle slice the greatest length possible for a given type and input slice, but only your algorithm's performance should depend on that, not its correctness.
In practice, returning max_value
happens when p as usize & (gcd - 1) != 0
(whatever exactly this means, this is taken from the implementation) -- and when running in Miri, which will always return max_value
.
Historically, Miri did this because it had no notion of the "integer address" of an allocation, so there literally was no way to offset a pointer to get a particular alignment. This is changing now, Miri is getting the support for this. So maybe we should note some conditions under which align_offset
will definitely succeed. A motivation for this is #61339, which implicitly made the assumption that aligning with size_of::<T>() == 1
will always succeed.
On the other hand, the current contract for align_offset
lets Miri do more reliable alignment checking. This is off-by-default but can be enabled with -Zmiri-symbolic-alignment-check
: when checking whether some pointer p
that points to offset o
inside an allocation with alignment a
, we have the option to consider only a
and o
and not the integer value of p
. This allows us to reliably detect alignment problems in code such as:
fn main() {
let x = &mut [0u8; 3];
let base_addr = x as *mut _ as usize;
let u16_ref = &mut *(base_addr as *mut u16);
*u16_ref = 2;
println!("{:?}", x);
}
If we were to take the actual integer value of p
into account, the program might get "lucky" and actually run successfully in Miri because base_addr
happens to be even. In contrast, by not doing this, Miri can offer a flag where the bug in the program above is definitely caught. With this flag, the user can be sure that none of the accesses in the program are aligned "by chance".
However, this also means that when this flag is set, the following code will not pass:
fn main() {
let x = &mut [0u8; 3];
let base_addr = x as *mut _ as usize;
let u16_ref = unsafe { if base_addr % 2 == 0 {
&mut *(base_addr as *mut u16)
} else {
&mut *((base_addr+1) as *mut u16)
} };
*u16_ref = 2;
println!("{:?}", x);
}
Miri cannot know that you actually did your homework and checked the integer address. This program is basically indistinguishable from the bad program above.
Currently there does not seem to be much code that operates like the last example above -- code will instead use align_to
, which will (when run in Miri with symbolic alignment checking) make the middle part empty, and thus the "higher-aligned" accesses just don't happen. This means the vast majority of code works fine in the better-alignment-checking mode. If we force align_offset
to not fail like #61339 expects, then suddenly align_to
will return non-empty middle parts in Miri as well, and -Zmiri-symbolic-alignment-check
will basically be useless. There will be false positives when any method using align_to
is called, which includes a few fundamental methods in libcore.
So, here's the trade-off: either Miri has a mode that can reliably detect alignment problems, or align_offset
guarantees success under certain conditions. I don't think we can have both. Which one do we pick?
(The particular PR #61339 that triggered this might be fixable to work with the contract Miri needs, I don't know. But this discussion will probably come up again.)
Cc @rust-lang/libs @rust-lang/wg-unsafe-code-guidelines