Skip to content

Commit 8358179

Browse files
Ban subnormals and NaNs in const {from,to}_bits
1 parent b200483 commit 8358179

File tree

5 files changed

+392
-38
lines changed

5 files changed

+392
-38
lines changed

library/core/src/num/f32.rs

+112-4
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,23 @@ impl f32 {
623623
}
624624
}
625625

626+
// This operates on bits, and only bits, so it can ignore concerns about weird FPUs.
627+
// FIXME(jubilee): In a just world, this would be the entire impl for classify,
628+
// plus a transmute. We do not live in a just world, but we can make it more so.
629+
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
630+
const fn classify_bits(b: u32) -> FpCategory {
631+
const EXP_MASK: u32 = 0x7f800000;
632+
const MAN_MASK: u32 = 0x007fffff;
633+
634+
match (b & MAN_MASK, b & EXP_MASK) {
635+
(0, EXP_MASK) => FpCategory::Infinite,
636+
(_, EXP_MASK) => FpCategory::Nan,
637+
(0, 0) => FpCategory::Zero,
638+
(_, 0) => FpCategory::Subnormal,
639+
_ => FpCategory::Normal,
640+
}
641+
}
642+
626643
/// Returns `true` if `self` has a positive sign, including `+0.0`, `NaN`s with
627644
/// positive sign bit and positive infinity.
628645
///
@@ -874,8 +891,59 @@ impl f32 {
874891
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
875892
#[inline]
876893
pub const fn to_bits(self) -> u32 {
877-
// SAFETY: `u32` is a plain old datatype so we can always transmute to it
878-
unsafe { mem::transmute(self) }
894+
// SAFETY: `u32` is a plain old datatype so we can always transmute to it.
895+
// ...sorta.
896+
//
897+
// It turns out that at runtime, it is possible for a floating point number
898+
// to be subject to a floating point mode that alters nonzero subnormal numbers
899+
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
900+
// This is not a problem per se, but at least one tier2 platform for Rust
901+
// actually exhibits this behavior by default.
902+
//
903+
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
904+
// i.e. not soft-float, the way Rust does parameter passing can actually alter
905+
// a number that is "not infinity" to have the same exponent as infinity,
906+
// in a slightly unpredictable manner.
907+
//
908+
// And, of course evaluating to a NaN value is fairly nondeterministic.
909+
// More precisely: when NaN should be returned is knowable, but which NaN?
910+
// So far that's defined by a combination of LLVM and the CPU, not Rust.
911+
// This function, however, allows observing the bitstring of a NaN,
912+
// thus introspection on CTFE.
913+
//
914+
// In order to preserve, at least for the moment, const-to-runtime equivalence,
915+
// we reject any of these possible situations from happening.
916+
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
917+
const fn ct_f32_to_u32(ct: f32) -> u32 {
918+
match ct.classify() {
919+
FpCategory::Nan => {
920+
panic!("const-eval error: cannot use f32::to_bits on a NaN")
921+
}
922+
FpCategory::Subnormal => {
923+
panic!("const-eval error: cannot use f32::to_bits on a subnormal number")
924+
}
925+
FpCategory::Infinite =>
926+
// SAFETY: Infinity per se is fine
927+
unsafe { mem::transmute::<f32, u32>(ct) },
928+
FpCategory::Zero | FpCategory::Normal => {
929+
// SAFETY: We have a normal floating point number. Now we transmute, i.e. do a bitcopy.
930+
let bits: u32 = unsafe { mem::transmute::<f32, u32>(ct) };
931+
// Let's doublecheck to make sure it wasn't a weird float by truncating it.
932+
if bits >> 23 & 0xFF == 0xFF {
933+
panic!(
934+
"const-eval error: an unusually large x87 floating point value should not leak into const eval"
935+
)
936+
};
937+
bits
938+
}
939+
}
940+
}
941+
// SAFETY: `u32` is a plain old datatype so we can always... uh...
942+
// ...look, just pretend you forgot what you just read.
943+
// Stability concerns.
944+
let rt_f32_to_u32 = |rt| unsafe { mem::transmute::<f32, u32>(rt) };
945+
// SAFETY: We use internal implementations that either always work or fail at compile time.
946+
unsafe { intrinsics::const_eval_select((self,), ct_f32_to_u32, rt_f32_to_u32) }
879947
}
880948

881949
/// Raw transmutation from `u32`.
@@ -919,9 +987,49 @@ impl f32 {
919987
#[must_use]
920988
#[inline]
921989
pub const fn from_bits(v: u32) -> Self {
922-
// SAFETY: `u32` is a plain old datatype so we can always transmute from it
923990
// It turns out the safety issues with sNaN were overblown! Hooray!
924-
unsafe { mem::transmute::<u32, f32>(v) }
991+
// SAFETY: `u32` is a plain old datatype so we can always transmute from it
992+
// ...sorta.
993+
//
994+
// It turns out that at runtime, it is possible for a floating point number
995+
// to be subject to a floating point mode that alters nonzero subnormal numbers
996+
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
997+
// This is not a problem usually, but at least one tier2 platform for Rust
998+
// actually exhibits this behavior by default: thumbv7neon
999+
// aka "the Neon FPU in AArch32 state"
1000+
//
1001+
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
1002+
// i.e. not soft-float, the way Rust does parameter passing can actually alter
1003+
// a number that is "not infinity" to have the same exponent as infinity,
1004+
// in a slightly unpredictable manner.
1005+
//
1006+
// And, of course evaluating to a NaN value is fairly nondeterministic.
1007+
// More precisely: when NaN should be returned is knowable, but which NaN?
1008+
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1009+
// This function, however, allows observing the bitstring of a NaN,
1010+
// thus introspection on CTFE.
1011+
//
1012+
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1013+
// reject any of these possible situations from happening.
1014+
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1015+
const fn ct_u32_to_f32(ct: u32) -> f32 {
1016+
match f32::classify_bits(ct) {
1017+
FpCategory::Subnormal => {
1018+
panic!("const-eval error: cannot use f32::from_bits on a subnormal number");
1019+
}
1020+
FpCategory::Nan => {
1021+
panic!("const-eval error: cannot use f32::from_bits on NaN");
1022+
}
1023+
// SAFETY: It's not a frumious number
1024+
_ => unsafe { mem::transmute::<u32, f32>(ct) },
1025+
}
1026+
}
1027+
// SAFETY: `u32` is a plain old datatype so we can always... uh...
1028+
// ...look, just pretend you forgot what you just read.
1029+
// Stability concerns.
1030+
let rt_u32_to_f32 = |rt| unsafe { mem::transmute::<u32, f32>(rt) };
1031+
// SAFETY: We use internal implementations that either always work or fail at compile time.
1032+
unsafe { intrinsics::const_eval_select((v,), ct_u32_to_f32, rt_u32_to_f32) }
9251033
}
9261034

9271035
/// Return the memory representation of this floating point number as a byte array in

library/core/src/num/f64.rs

+99-4
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,23 @@ impl f64 {
621621
}
622622
}
623623

624+
// This operates on bits, and only bits, so it can ignore concerns about weird FPUs.
625+
// FIXME(jubilee): In a just world, this would be the entire impl for classify,
626+
// plus a transmute. We do not live in a just world, but we can make it more so.
627+
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
628+
const fn classify_bits(b: u64) -> FpCategory {
629+
const EXP_MASK: u64 = 0x7ff0000000000000;
630+
const MAN_MASK: u64 = 0x000fffffffffffff;
631+
632+
match (b & MAN_MASK, b & EXP_MASK) {
633+
(0, EXP_MASK) => FpCategory::Infinite,
634+
(_, EXP_MASK) => FpCategory::Nan,
635+
(0, 0) => FpCategory::Zero,
636+
(_, 0) => FpCategory::Subnormal,
637+
_ => FpCategory::Normal,
638+
}
639+
}
640+
624641
/// Returns `true` if `self` has a positive sign, including `+0.0`, `NaN`s with
625642
/// positive sign bit and positive infinity.
626643
///
@@ -891,8 +908,41 @@ impl f64 {
891908
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
892909
#[inline]
893910
pub const fn to_bits(self) -> u64 {
894-
// SAFETY: `u64` is a plain old datatype so we can always transmute to it
895-
unsafe { mem::transmute(self) }
911+
// SAFETY: `u64` is a plain old datatype so we can always transmute to it.
912+
// ...sorta.
913+
//
914+
// See the SAFETY comment in f64::from_bits for more.
915+
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
916+
const fn ct_f64_to_u64(ct: f64) -> u64 {
917+
match ct.classify() {
918+
FpCategory::Nan => {
919+
panic!("const-eval error: cannot use f64::to_bits on a NaN")
920+
}
921+
FpCategory::Subnormal => {
922+
panic!("const-eval error: cannot use f64::to_bits on a subnormal number")
923+
}
924+
FpCategory::Infinite =>
925+
// SAFETY: Infinity per se is fine
926+
unsafe { mem::transmute::<f64, u64>(ct) },
927+
FpCategory::Zero | FpCategory::Normal => {
928+
// SAFETY: We have a normal floating point number. Now we transmute, i.e. do a bitcopy.
929+
let bits: u64 = unsafe { mem::transmute::<f64, u64>(ct) };
930+
// Let's doublecheck to make sure it wasn't a weird float by truncating it.
931+
if (bits >> 52) & 0x7FF == 0x7FF {
932+
panic!(
933+
"const-eval error: an unusually large x87 floating point value should not leak into const eval"
934+
)
935+
};
936+
bits
937+
}
938+
}
939+
}
940+
// SAFETY: `u64` is a plain old datatype so we can always... uh...
941+
// ...look, just pretend you forgot what you just read.
942+
// Stability concerns.
943+
let rt_f64_to_u64 = |rt| unsafe { mem::transmute::<f64, u64>(rt) };
944+
// SAFETY: We use internal implementations that either always work or fail at compile time.
945+
unsafe { intrinsics::const_eval_select((self,), ct_f64_to_u64, rt_f64_to_u64) }
896946
}
897947

898948
/// Raw transmutation from `u64`.
@@ -936,9 +986,54 @@ impl f64 {
936986
#[must_use]
937987
#[inline]
938988
pub const fn from_bits(v: u64) -> Self {
939-
// SAFETY: `u64` is a plain old datatype so we can always transmute from it
940989
// It turns out the safety issues with sNaN were overblown! Hooray!
941-
unsafe { mem::transmute::<u64, f64>(v) }
990+
// SAFETY: `u64` is a plain old datatype so we can always transmute from it
991+
// ...sorta.
992+
//
993+
// It turns out that at runtime, it is possible for a floating point number
994+
// to be subject to floating point modes that alters nonzero subnormal numbers
995+
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
996+
// This is not a problem usually, but at least one tier2 platform for Rust
997+
// actually exhibits an FTZ behavior kby default: thumbv7neon
998+
// aka "the Neon FPU in AArch32 state"
999+
//
1000+
// Even with this, not all instructions exhibit the FTZ behaviors on thumbv7neon,
1001+
// so this should load the same bits if LLVM emits the "correct" instructions,
1002+
// but LLVM sometimes makes interesting choices about float optimization,
1003+
// and other FPUs may do similar. Thus, it is wise to indulge luxuriously in caution.
1004+
//
1005+
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
1006+
// i.e. not soft-float, the way Rust does parameter passing can actually alter
1007+
// a number that is "not infinity" to have the same exponent as infinity,
1008+
// in a slightly unpredictable manner.
1009+
//
1010+
// And, of course evaluating to a NaN value is fairly nondeterministic.
1011+
// More precisely: when NaN should be returned is knowable, but which NaN?
1012+
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1013+
// This function, however, allows observing the bitstring of a NaN,
1014+
// thus introspection on CTFE.
1015+
//
1016+
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1017+
// reject any of these possible situations from happening.
1018+
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1019+
const fn ct_u64_to_f64(ct: u64) -> f64 {
1020+
match f64::classify_bits(ct) {
1021+
FpCategory::Subnormal => {
1022+
panic!("const-eval error: cannot use f64::from_bits on a subnormal number");
1023+
}
1024+
FpCategory::Nan => {
1025+
panic!("const-eval error: cannot use f64::from_bits on NaN");
1026+
}
1027+
// SAFETY: It's not a frumious number
1028+
_ => unsafe { mem::transmute::<u64, f64>(ct) },
1029+
}
1030+
}
1031+
// SAFETY: `u64` is a plain old datatype so we can always... uh...
1032+
// ...look, just pretend you forgot what you just read.
1033+
// Stability concerns.
1034+
let rt_u64_to_f64 = |rt| unsafe { mem::transmute::<u64, f64>(rt) };
1035+
// SAFETY: We use internal implementations that either always work or fail at compile time.
1036+
unsafe { intrinsics::const_eval_select((v,), ct_u64_to_f64, rt_u64_to_f64) }
9421037
}
9431038

9441039
/// Return the memory representation of this floating point number as a byte array in

src/test/ui/consts/const-float-bits-conv.rs

-30
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,6 @@ fn f32() {
3737
const_assert!(f32::from_bits(0x44a72000), 1337.0);
3838
const_assert!(f32::from_ne_bytes(0x44a72000u32.to_ne_bytes()), 1337.0);
3939
const_assert!(f32::from_bits(0xc1640000), -14.25);
40-
41-
// Check that NaNs roundtrip their bits regardless of signalingness
42-
// 0xA is 0b1010; 0x5 is 0b0101 -- so these two together clobbers all the mantissa bits
43-
const MASKED_NAN1: u32 = f32::NAN.to_bits() ^ 0x002A_AAAA;
44-
const MASKED_NAN2: u32 = f32::NAN.to_bits() ^ 0x0055_5555;
45-
46-
const_assert!(f32::from_bits(MASKED_NAN1).is_nan());
47-
const_assert!(f32::from_bits(MASKED_NAN1).is_nan());
48-
49-
// LLVM does not guarantee that loads and stores of NaNs preserve their exact bit pattern.
50-
// In practice, this seems to only cause a problem on x86, since the most widely used calling
51-
// convention mandates that floating point values are returned on the x87 FPU stack. See #73328.
52-
if !cfg!(target_arch = "x86") {
53-
const_assert!(f32::from_bits(MASKED_NAN1).to_bits(), MASKED_NAN1);
54-
const_assert!(f32::from_bits(MASKED_NAN2).to_bits(), MASKED_NAN2);
55-
}
5640
}
5741

5842
fn f64() {
@@ -70,20 +54,6 @@ fn f64() {
7054
const_assert!(f64::from_bits(0x4094e40000000000), 1337.0);
7155
const_assert!(f64::from_ne_bytes(0x4094e40000000000u64.to_ne_bytes()), 1337.0);
7256
const_assert!(f64::from_bits(0xc02c800000000000), -14.25);
73-
74-
// Check that NaNs roundtrip their bits regardless of signalingness
75-
// 0xA is 0b1010; 0x5 is 0b0101 -- so these two together clobbers all the mantissa bits
76-
const MASKED_NAN1: u64 = f64::NAN.to_bits() ^ 0x000A_AAAA_AAAA_AAAA;
77-
const MASKED_NAN2: u64 = f64::NAN.to_bits() ^ 0x0005_5555_5555_5555;
78-
79-
const_assert!(f64::from_bits(MASKED_NAN1).is_nan());
80-
const_assert!(f64::from_bits(MASKED_NAN1).is_nan());
81-
82-
// See comment above.
83-
if !cfg!(target_arch = "x86") {
84-
const_assert!(f64::from_bits(MASKED_NAN1).to_bits(), MASKED_NAN1);
85-
const_assert!(f64::from_bits(MASKED_NAN2).to_bits(), MASKED_NAN2);
86-
}
8757
}
8858

8959
fn main() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// compile-flags: -Zmir-opt-level=0
2+
#![feature(const_float_bits_conv)]
3+
#![feature(const_float_classify)]
4+
5+
// Don't promote
6+
const fn nop<T>(x: T) -> T { x }
7+
8+
macro_rules! const_assert {
9+
($a:expr) => {
10+
{
11+
const _: () = assert!($a);
12+
assert!(nop($a));
13+
}
14+
};
15+
($a:expr, $b:expr) => {
16+
{
17+
const _: () = assert!($a == $b);
18+
assert_eq!(nop($a), nop($b));
19+
}
20+
};
21+
}
22+
23+
fn f32() {
24+
// Check that NaNs roundtrip their bits regardless of signalingness
25+
// 0xA is 0b1010; 0x5 is 0b0101 -- so these two together clobbers all the mantissa bits
26+
// ...actually, let's just check that these break. :D
27+
const MASKED_NAN1: u32 = f32::NAN.to_bits() ^ 0x002A_AAAA;
28+
const MASKED_NAN2: u32 = f32::NAN.to_bits() ^ 0x0055_5555;
29+
30+
const_assert!(f32::from_bits(MASKED_NAN1).is_nan());
31+
const_assert!(f32::from_bits(MASKED_NAN1).is_nan());
32+
33+
// LLVM does not guarantee that loads and stores of NaNs preserve their exact bit pattern.
34+
// In practice, this seems to only cause a problem on x86, since the most widely used calling
35+
// convention mandates that floating point values are returned on the x87 FPU stack. See #73328.
36+
if !cfg!(target_arch = "x86") {
37+
const_assert!(f32::from_bits(MASKED_NAN1).to_bits(), MASKED_NAN1);
38+
const_assert!(f32::from_bits(MASKED_NAN2).to_bits(), MASKED_NAN2);
39+
}
40+
}
41+
42+
fn f64() {
43+
// Check that NaNs roundtrip their bits regardless of signalingness
44+
// 0xA is 0b1010; 0x5 is 0b0101 -- so these two together clobbers all the mantissa bits
45+
// ...actually, let's just check that these break. :D
46+
const MASKED_NAN1: u64 = f64::NAN.to_bits() ^ 0x000A_AAAA_AAAA_AAAA;
47+
const MASKED_NAN2: u64 = f64::NAN.to_bits() ^ 0x0005_5555_5555_5555;
48+
49+
const_assert!(f64::from_bits(MASKED_NAN1).is_nan());
50+
const_assert!(f64::from_bits(MASKED_NAN1).is_nan());
51+
52+
// See comment above.
53+
if !cfg!(target_arch = "x86") {
54+
const_assert!(f64::from_bits(MASKED_NAN1).to_bits(), MASKED_NAN1);
55+
const_assert!(f64::from_bits(MASKED_NAN2).to_bits(), MASKED_NAN2);
56+
}
57+
}
58+
59+
fn main() {
60+
f32();
61+
f64();
62+
}

0 commit comments

Comments
 (0)