Skip to content

Commit 785ca7d

Browse files
committed
Move Option::as_slice to an always-sound implementation
This approach depends on CSE to not have any branches or selects when the guessed offset is correct -- which it always will be right now -- but to also be *sound* (just less efficient) if the layout algorithms change such that the guess is incorrect.
1 parent db2dc7e commit 785ca7d

File tree

1 file changed

+79
-37
lines changed

1 file changed

+79
-37
lines changed

core/src/option.rs

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -735,22 +735,43 @@ impl<T> Option<T> {
735735
}
736736
}
737737

738-
const fn get_some_offset() -> isize {
739-
if mem::size_of::<Option<T>>() == mem::size_of::<T>() {
740-
// niche optimization means the `T` is always stored at the same position as the Option.
741-
0
738+
/// This is a guess at how many bytes into the option the payload can be found.
739+
///
740+
/// For niche-optimized types it's correct because it's pigeon-holed to only
741+
/// one possible place. For other types, it's usually correct today, but
742+
/// tweaks to the layout algorithm (particularly expansions of
743+
/// `-Z randomize-layout`) might make it incorrect at any point.
744+
///
745+
/// It's guaranteed to be a multiple of alignment (so will always give a
746+
/// correctly-aligned location) and to be within the allocated object, so
747+
/// is valid to use with `offset` and to use for a zero-sized read.
748+
const SOME_BYTE_OFFSET_GUESS: isize = {
749+
let some_uninit = Some(mem::MaybeUninit::<T>::uninit());
750+
let payload_ref = some_uninit.as_ref().unwrap();
751+
// SAFETY: `as_ref` gives an address inside the existing `Option`,
752+
// so both pointers are derived from the same thing and the result
753+
// cannot overflow an `isize`.
754+
let offset = unsafe { <*const _>::byte_offset_from(payload_ref, &some_uninit) };
755+
756+
// The offset is into the object, so it's guaranteed to be non-negative.
757+
assert!(offset >= 0);
758+
759+
// The payload and the overall option are aligned,
760+
// so the offset will be a multiple of the alignment too.
761+
assert!((offset as usize) % mem::align_of::<T>() == 0);
762+
763+
let max_offset = mem::size_of::<Self>() - mem::size_of::<T>();
764+
if offset as usize <= max_offset {
765+
// The offset is at least inside the object, so let's try it.
766+
offset
742767
} else {
743-
assert!(mem::size_of::<Option<T>>() == mem::size_of::<Option<mem::MaybeUninit<T>>>());
744-
let some_uninit = Some(mem::MaybeUninit::<T>::uninit());
745-
// SAFETY: This gets the byte offset of the `Some(_)` value following the fact that
746-
// niche optimization is not active, and thus Option<T> and Option<MaybeUninit<t>> share
747-
// the same layout.
748-
unsafe {
749-
(some_uninit.as_ref().unwrap() as *const mem::MaybeUninit<T>)
750-
.byte_offset_from(&some_uninit as *const Option<mem::MaybeUninit<T>>)
751-
}
768+
// The offset guess is definitely wrong, so use the address
769+
// of the original option since we have it already.
770+
// This also correctly handles the case of layout-optimized enums
771+
// where `max_offset == 0` and thus this is the only possibility.
772+
0
752773
}
753-
}
774+
};
754775

755776
/// Returns a slice of the contained value, if any. If this is `None`, an
756777
/// empty slice is returned. This can be useful to have a single type of
@@ -784,18 +805,28 @@ impl<T> Option<T> {
784805
#[must_use]
785806
#[unstable(feature = "option_as_slice", issue = "108545")]
786807
pub fn as_slice(&self) -> &[T] {
787-
// SAFETY: This is sound as long as `get_some_offset` returns the
788-
// correct offset. Though in the `None` case, the slice may be located
789-
// at a pointer pointing into padding, the fact that the slice is
790-
// empty, and the padding is at a properly aligned position for a
791-
// value of that type makes it sound.
792-
unsafe {
793-
slice::from_raw_parts(
794-
(self as *const Option<T>).wrapping_byte_offset(Self::get_some_offset())
795-
as *const T,
796-
self.is_some() as usize,
797-
)
798-
}
808+
let payload_ptr: *const T =
809+
// The goal here is that both arms here are calculating exactly
810+
// the same pointer, and thus it'll be folded away when the guessed
811+
// offset is correct, but if the guess is wrong for some reason
812+
// it'll at least still be sound, just no longer optimal.
813+
if let Some(payload) = self {
814+
payload
815+
} else {
816+
let self_ptr: *const Self = self;
817+
// SAFETY: `SOME_BYTE_OFFSET_GUESS` guarantees that its value is
818+
// such that this will be in-bounds of the object.
819+
unsafe { self_ptr.byte_offset(Self::SOME_BYTE_OFFSET_GUESS).cast() }
820+
};
821+
let len = usize::from(self.is_some());
822+
823+
// SAFETY: When the `Option` is `Some`, we're using the actual pointer
824+
// to the payload, with a length of 1, so this is equivalent to
825+
// `slice::from_ref`, and thus is safe.
826+
// When the `Option` is `None`, the length used is 0, so to be safe it
827+
// just needs to be aligned, which it is because `&self` is aligned and
828+
// the offset used is a multiple of alignment.
829+
unsafe { slice::from_raw_parts(payload_ptr, len) }
799830
}
800831

801832
/// Returns a mutable slice of the contained value, if any. If this is
@@ -840,17 +871,28 @@ impl<T> Option<T> {
840871
#[must_use]
841872
#[unstable(feature = "option_as_slice", issue = "108545")]
842873
pub fn as_mut_slice(&mut self) -> &mut [T] {
843-
// SAFETY: This is sound as long as `get_some_offset` returns the
844-
// correct offset. Though in the `None` case, the slice may be located
845-
// at a pointer pointing into padding, the fact that the slice is
846-
// empty, and the padding is at a properly aligned position for a
847-
// value of that type makes it sound.
848-
unsafe {
849-
slice::from_raw_parts_mut(
850-
(self as *mut Option<T>).wrapping_byte_offset(Self::get_some_offset()) as *mut T,
851-
self.is_some() as usize,
852-
)
853-
}
874+
let payload_ptr: *mut T =
875+
// The goal here is that both arms here are calculating exactly
876+
// the same pointer, and thus it'll be folded away when the guessed
877+
// offset is correct, but if the guess is wrong for some reason
878+
// it'll at least still be sound, just no longer optimal.
879+
if let Some(payload) = self {
880+
payload
881+
} else {
882+
let self_ptr: *mut Self = self;
883+
// SAFETY: `SOME_BYTE_OFFSET_GUESS` guarantees that its value is
884+
// such that this will be in-bounds of the object.
885+
unsafe { self_ptr.byte_offset(Self::SOME_BYTE_OFFSET_GUESS).cast() }
886+
};
887+
let len = usize::from(self.is_some());
888+
889+
// SAFETY: When the `Option` is `Some`, we're using the actual pointer
890+
// to the payload, with a length of 1, so this is equivalent to
891+
// `slice::from_mut`, and thus is safe.
892+
// When the `Option` is `None`, the length used is 0, so to be safe it
893+
// just needs to be aligned, which it is because `&self` is aligned and
894+
// the offset used is a multiple of alignment.
895+
unsafe { slice::from_raw_parts_mut(payload_ptr, len) }
854896
}
855897

856898
/////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)