Remove unsound casting functions

The casting functions in `unsafe.rs` were unsound (i.e., they allowed
safe code to cause undefined behavior). While they did appear to be used
in a way that wouldn't cause UB the fact that there exists unsound
functions is unsettling.

This commit removes those functions and replaces it with a macro that
performs the same reification - the difference is that the macro call
will also include the checks which are required to prevent UB. A macro
was chosen instead of a function for two reasons:

1. A macro can keep the same code generation whereas a function would
   require going through an `Option` which has negative impacts on code
   generation (niche values cause poor DCE).
2. There exist other `unsafe` code blocks in the crate and an attempt to
   make Rhai 100% safe is completely out-of-scope for this merge
   request, so we may as well use `unsafe` in the macro.

Regarding (2) above, I may come back at a later date with a 100% safe
`reify` function but only once the other `unsafe` blocks are removed.
For posterity, said function would look something like:

```rust
fn reify<A: Any, C>(value: A) -> Option<C> {
    let mut v = Some(value);
    let v: &mut dyn Any = &mut v;
    v.downcast_mut::<Option<C>>().map(Option::take)
}
```
This commit is contained in:
Nathan Kent 2022-02-05 16:29:05 -08:00
parent 6a740a9fa1
commit 86d86a85e4
7 changed files with 94 additions and 266 deletions

View File

@ -11,7 +11,7 @@ Root Sources
| `tokenizer.rs` | Script tokenizer/lexer | | `tokenizer.rs` | Script tokenizer/lexer |
| `parser.rs` | Script parser | | `parser.rs` | Script parser |
| `optimizer.rs` | Script optimizer | | `optimizer.rs` | Script optimizer |
| `unsafe.rs` | `unsafe` functions | | `reify.rs` | Utilities for making generic types concrete |
| `tests.rs` | Unit tests (not integration tests, which are in the `rhai/tests` sub-directory) | | `tests.rs` | Unit tests (not integration tests, which are in the `rhai/tests` sub-directory) |

View File

@ -3,16 +3,15 @@
use crate::ast::Expr; use crate::ast::Expr;
use crate::func::native::SendSync; use crate::func::native::SendSync;
use crate::parser::ParseResult; use crate::parser::ParseResult;
use crate::r#unsafe::unsafe_try_cast;
use crate::tokenizer::{is_valid_identifier, Token}; use crate::tokenizer::{is_valid_identifier, Token};
use crate::types::dynamic::Variant; use crate::types::dynamic::Variant;
use crate::{ use crate::{
Engine, EvalContext, Identifier, ImmutableString, LexError, Position, RhaiResult, Shared, Engine, EvalContext, Identifier, ImmutableString, LexError, Position, RhaiResult, Shared,
StaticVec, INT, StaticVec, reify,
}; };
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
use std::{any::TypeId, ops::Deref}; use std::ops::Deref;
/// Collection of special markers for custom syntax definition. /// Collection of special markers for custom syntax definition.
pub mod markers { pub mod markers {
@ -94,46 +93,23 @@ impl Expression<'_> {
#[must_use] #[must_use]
pub fn get_literal_value<T: Variant>(&self) -> Option<T> { pub fn get_literal_value<T: Variant>(&self) -> Option<T> {
// Coded this way in order to maximally leverage potentials for dead-code removal. // Coded this way in order to maximally leverage potentials for dead-code removal.
match self.0 {
Expr::IntegerConstant(x, _) => reify!(x, |x: T| Some(x), || None),
if TypeId::of::<T>() == TypeId::of::<INT>() { #[cfg(not(feature = "no_float"))]
return match self.0 { Expr::FloatConstant(x, _) => reify!(x, |x: T| Some(x), || None),
Expr::IntegerConstant(x, _) => unsafe_try_cast(*x),
_ => None, Expr::CharConstant(x, _) => reify!(x, |x: T| Some(x), || None),
}; Expr::StringConstant(x, _) => reify!(x.clone(), |x: T| Some(x), || None),
Expr::Variable(_, _, x) => {
let x = Into::<ImmutableString>::into(&x.2);
reify!(x, |x: T| Some(x), || None)
}
Expr::BoolConstant(x, _) => reify!(x, |x: T| Some(x), || None),
Expr::Unit(_) => reify!((), |x: T| Some(x), || None),
_ => None,
} }
#[cfg(not(feature = "no_float"))]
if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
return match self.0 {
Expr::FloatConstant(x, _) => unsafe_try_cast(*x),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<char>() {
return match self.0 {
Expr::CharConstant(x, _) => unsafe_try_cast(*x),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
return match self.0 {
Expr::StringConstant(x, _) => unsafe_try_cast(x.clone()),
Expr::Variable(_, _, x) => unsafe_try_cast(Into::<ImmutableString>::into(&x.2)),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<bool>() {
return match self.0 {
Expr::BoolConstant(x, _) => unsafe_try_cast(*x),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<()>() {
return match self.0 {
Expr::Unit(_) => unsafe_try_cast(()),
_ => None,
};
}
None
} }
} }

View File

@ -5,10 +5,9 @@
use super::call::FnCallArgs; use super::call::FnCallArgs;
use super::callable_function::CallableFunction; use super::callable_function::CallableFunction;
use super::native::{FnAny, SendSync}; use super::native::{FnAny, SendSync};
use crate::r#unsafe::unsafe_cast;
use crate::tokenizer::Position; use crate::tokenizer::Position;
use crate::types::dynamic::{DynamicWriteLock, Variant}; use crate::types::dynamic::{DynamicWriteLock, Variant};
use crate::{Dynamic, NativeCallContext, RhaiResultOf, ERR}; use crate::{Dynamic, NativeCallContext, RhaiResultOf, ERR, reify};
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
use std::{any::TypeId, mem}; use std::{any::TypeId, mem};
@ -46,11 +45,12 @@ pub fn by_value<T: Variant + Clone>(data: &mut Dynamic) -> T {
// If T is `&str`, data must be `ImmutableString`, so map directly to it // If T is `&str`, data must be `ImmutableString`, so map directly to it
data.flatten_in_place(); data.flatten_in_place();
let ref_str = data.as_str_ref().expect("&str"); let ref_str = data.as_str_ref().expect("&str");
let ref_t = unsafe { mem::transmute::<_, &T>(&ref_str) }; let ref_t = reify!(ref_str, |ref_t: &T| ref_t, || unreachable!());
ref_t.clone() ref_t.clone()
} else if TypeId::of::<T>() == TypeId::of::<String>() { } else if TypeId::of::<T>() == TypeId::of::<String>() {
// If T is `String`, data must be `ImmutableString`, so map directly to it // If T is `String`, data must be `ImmutableString`, so map directly to it
unsafe_cast(mem::take(data).into_string().expect("`ImmutableString`")) let t = mem::take(data).into_string().expect("`ImmutableString`");
reify!(t, |t: T| t, || unreachable!())
} else { } else {
// We consume the argument and then replace it with () - the argument is not supposed to be used again. // We consume the argument and then replace it with () - the argument is not supposed to be used again.
// This way, we avoid having to clone the argument again, because it is already a clone when passed here. // This way, we avoid having to clone the argument again, because it is already a clone when passed here.

View File

@ -68,6 +68,7 @@ extern crate no_std_compat as std;
use std::prelude::v1::*; use std::prelude::v1::*;
// Internal modules // Internal modules
mod reify;
mod api; mod api;
mod ast; mod ast;
@ -81,7 +82,6 @@ mod parser;
mod tests; mod tests;
mod tokenizer; mod tokenizer;
mod types; mod types;
mod r#unsafe;
/// Error encountered when parsing a script. /// Error encountered when parsing a script.
type PERR = ParseErrorType; type PERR = ParseErrorType;

26
src/reify.rs Normal file
View File

@ -0,0 +1,26 @@
/// Runs `$code` if `$old` is of type `$t`.
#[macro_export]
macro_rules! reify {
($old:ident, |$new:ident : $t:ty| $code:expr, || $fallback:expr) => {{
#[allow(unused_imports)]
use ::std::{any::{Any, TypeId}, mem::{ManuallyDrop, transmute_copy}};
if TypeId::of::<$t>() == $old.type_id() {
// SAFETY: This is safe because we check to make sure the two types are
// actually the same type.
let $new: $t = unsafe { transmute_copy(&ManuallyDrop::new($old)) };
$code
} else {
$fallback
}
}};
($old:expr, |$new:ident : $t:ty| $code:expr, || $fallback:expr) => {{
let old = $old;
reify!(old, |$new : $t| $code, || $fallback)
}};
($old:ident, |$new:ident : $t:ty| $code:expr) => {
reify!($old, |$new : $t| $code, || ())
};
($old:expr, |$new:ident : $t:ty| $code:expr) => {
reify!($old, |$new : $t| $code, || ())
};
}

View File

@ -1,8 +1,7 @@
//! Helper module which defines the [`Any`] trait to to allow dynamic value handling. //! Helper module which defines the [`Any`] trait to to allow dynamic value handling.
use crate::func::native::SendSync; use crate::func::native::SendSync;
use crate::r#unsafe::{unsafe_cast, unsafe_cast_box, unsafe_try_cast}; use crate::{ExclusiveRange, FnPtr, ImmutableString, InclusiveRange, INT, reify};
use crate::{ExclusiveRange, FnPtr, ImmutableString, InclusiveRange, INT};
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
use std::{ use std::{
@ -1137,10 +1136,6 @@ impl Dynamic {
} }
/// Create a [`Dynamic`] from any type. A [`Dynamic`] value is simply returned as is. /// Create a [`Dynamic`] from any type. A [`Dynamic`] value is simply returned as is.
/// ///
/// # Safety
///
/// This type uses some unsafe code, mainly for type casting.
///
/// # Notes /// # Notes
/// ///
/// Beware that you need to pass in an [`Array`][crate::Array] type for it to be recognized as an [`Array`][crate::Array]. /// Beware that you need to pass in an [`Array`][crate::Array] type for it to be recognized as an [`Array`][crate::Array].
@ -1175,74 +1170,37 @@ impl Dynamic {
pub fn from<T: Variant + Clone>(value: T) -> Self { pub fn from<T: Variant + Clone>(value: T) -> Self {
// Coded this way in order to maximally leverage potentials for dead-code removal. // Coded this way in order to maximally leverage potentials for dead-code removal.
if TypeId::of::<T>() == TypeId::of::<Dynamic>() { reify!(value, |v: Dynamic| return v);
return unsafe_cast::<_, Dynamic>(value); reify!(value, |v: INT| return v.into());
}
let val = value.as_any();
if TypeId::of::<T>() == TypeId::of::<INT>() {
return (*val.downcast_ref::<INT>().expect(CHECKED)).into();
}
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() { reify!(value, |v: crate::FLOAT| return v.into());
return (*val.downcast_ref::<crate::FLOAT>().expect(CHECKED)).into();
}
#[cfg(feature = "decimal")] #[cfg(feature = "decimal")]
if TypeId::of::<T>() == TypeId::of::<rust_decimal::Decimal>() { reify!(value, |v: rust_decimal::Decimal| return v.into());
return (*val.downcast_ref::<rust_decimal::Decimal>().expect(CHECKED)).into();
}
if TypeId::of::<T>() == TypeId::of::<bool>() {
return (*val.downcast_ref::<bool>().expect(CHECKED)).into();
}
if TypeId::of::<T>() == TypeId::of::<char>() {
return (*val.downcast_ref::<char>().expect(CHECKED)).into();
}
if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
return val
.downcast_ref::<ImmutableString>()
.expect(CHECKED)
.clone()
.into();
}
if TypeId::of::<T>() == TypeId::of::<&str>() {
return val.downcast_ref::<&str>().expect(CHECKED).deref().into();
}
if TypeId::of::<T>() == TypeId::of::<()>() {
return ().into();
}
if TypeId::of::<T>() == TypeId::of::<String>() { reify!(value, |v: bool| return v.into());
return unsafe_cast::<_, String>(value).into(); reify!(value, |v: char| return v.into());
} reify!(value, |v: ImmutableString| return v.into());
#[cfg(not(feature = "no_float"))] reify!(value, |v: &str| return v.into());
if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() { reify!(value, |v: ()| return v.into());
return unsafe_cast::<_, crate::FLOAT>(value).into();
} reify!(value, |v: String| return v.into());
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<crate::Array>() { reify!(value, |v: crate::Array| return v.into());
return unsafe_cast::<_, crate::Array>(value).into();
}
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<crate::Blob>() { reify!(value, |v: crate::Blob| {
return Dynamic::from_blob(unsafe_cast::<_, crate::Blob>(value)); // don't use blob.into() because it'll be converted into an Array // don't use blob.into() because it'll be converted into an Array
} return Dynamic::from_blob(v);
});
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
if TypeId::of::<T>() == TypeId::of::<crate::Map>() { reify!(value, |v: crate::Map| return v.into());
return unsafe_cast::<_, crate::Map>(value).into(); reify!(value, |v: FnPtr| return v.into());
}
if TypeId::of::<T>() == TypeId::of::<FnPtr>() {
return unsafe_cast::<_, FnPtr>(value).into();
}
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
if TypeId::of::<T>() == TypeId::of::<Instant>() { reify!(value, |v: Instant| return v.into());
return unsafe_cast::<_, Instant>(value).into();
}
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
if TypeId::of::<T>() == TypeId::of::<crate::Shared<crate::Locked<Dynamic>>>() { reify!(value, |v: crate::Shared<crate::Locked<Dynamic>>| return v.into());
return unsafe_cast::<_, crate::Shared<crate::Locked<Dynamic>>>(value).into();
}
Self(Union::Variant( Self(Union::Variant(
Box::new(Box::new(value)), Box::new(Box::new(value)),
@ -1311,112 +1269,34 @@ impl Dynamic {
return self.flatten().try_cast::<T>(); return self.flatten().try_cast::<T>();
} }
if TypeId::of::<T>() == TypeId::of::<Dynamic>() { reify!(self, |v: T| return Some(v));
return unsafe_try_cast::<_, T>(self);
}
if TypeId::of::<T>() == TypeId::of::<INT>() {
return match self.0 {
Union::Int(v, _, _) => unsafe_try_cast(v),
_ => None,
};
}
#[cfg(not(feature = "no_float"))]
if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
return match self.0 {
Union::Float(v, _, _) => unsafe_try_cast(*v),
_ => None,
};
}
#[cfg(feature = "decimal")]
if TypeId::of::<T>() == TypeId::of::<rust_decimal::Decimal>() {
return match self.0 {
Union::Decimal(v, _, _) => unsafe_try_cast(*v),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<bool>() {
return match self.0 {
Union::Bool(v, _, _) => unsafe_try_cast(v),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
return match self.0 {
Union::Str(v, _, _) => unsafe_try_cast(v),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<String>() {
return match self.0 {
Union::Str(v, _, _) => unsafe_try_cast(v.to_string()),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<char>() {
return match self.0 {
Union::Char(v, _, _) => unsafe_try_cast(v),
_ => None,
};
}
#[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<crate::Array>() {
return match self.0 {
Union::Array(v, _, _) => unsafe_cast_box::<_, T>(v),
_ => None,
};
}
#[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<crate::Blob>() {
return match self.0 {
Union::Blob(v, _, _) => unsafe_cast_box::<_, T>(v),
_ => None,
};
}
#[cfg(not(feature = "no_object"))]
if TypeId::of::<T>() == TypeId::of::<crate::Map>() {
return match self.0 {
Union::Map(v, _, _) => unsafe_cast_box::<_, T>(v),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<FnPtr>() {
return match self.0 {
Union::FnPtr(v, _, _) => unsafe_cast_box::<_, T>(v),
_ => None,
};
}
#[cfg(not(feature = "no_std"))]
if TypeId::of::<T>() == TypeId::of::<Instant>() {
return match self.0 {
Union::TimeStamp(v, _, _) => unsafe_cast_box::<_, T>(v),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<()>() {
return match self.0 {
Union::Unit(v, _, _) => unsafe_try_cast(v),
_ => None,
};
}
match self.0 { match self.0 {
Union::Int(v, ..) => reify!(v, |v: T| Some(v), || None),
#[cfg(not(feature = "no_float"))]
Union::Float(v, ..) => reify!(v, |v: T| Some(v), || None),
#[cfg(feature = "decimal")]
Union::Decimal(v, ..) => reify!(v, |v: T| Some(v), || None),
Union::Bool(v, ..) => reify!(v, |v: T| Some(v), || None),
Union::Str(v, ..) => {
reify!(v, |v: T| Some(v), || {
reify!(v.to_string(), |v: T| Some(v), || None)
})
},
Union::Char(v, ..) => reify!(v, |v: T| Some(v), || None),
#[cfg(not(feature = "no_index"))]
Union::Array(v, ..) => reify!(v, |v: Box<T>| Some(*v), || None),
#[cfg(not(feature = "no_index"))]
Union::Blob(v, ..) => reify!(v, |v: Box<T>| Some(*v), || None),
#[cfg(not(feature = "no_object"))]
Union::Map(v, ..) => reify!(v, |v: Box<T>| Some(*v), || None),
Union::FnPtr(v, ..) => reify!(v, |v: Box<T>| Some(*v), || None),
#[cfg(not(feature = "no_std"))]
Union::TimeStamp(v, ..) => reify!(v, |v: Box<T>| Some(*v), || None),
Union::Unit(v, ..) => reify!(v, |v: T| Some(v), || None),
Union::Variant(v, _, _) => (*v).as_boxed_any().downcast().ok().map(|x| *x), Union::Variant(v, _, _) => (*v).as_boxed_any().downcast().ok().map(|x| *x),
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
Union::Shared(_, _, _) => unreachable!("Union::Shared case should be already handled"), Union::Shared(_, _, _) => unreachable!("Union::Shared case should be already handled"),
_ => None,
} }
} }
/// Convert the [`Dynamic`] value into a specific type. /// Convert the [`Dynamic`] value into a specific type.

View File

@ -1,54 +0,0 @@
//! A helper module containing unsafe utility functions.
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use std::{
any::{Any, TypeId},
mem, ptr,
};
/// Cast a type into another type.
///
/// # Undefined Behavior
///
/// It is UB if the types are not compatible.
#[inline(always)]
#[must_use]
pub fn unsafe_cast<A: Any, B: Any>(a: A) -> B {
unsafe {
let ret: B = ptr::read(&a as *const _ as *const B);
// We explicitly forget the value immediately after moving out,
// removing any chance of a destructor running or value otherwise
// being used again.
mem::forget(a);
ret
}
}
/// Cast a type into another type.
#[inline(always)]
#[must_use]
pub fn unsafe_try_cast<A: Any, B: Any>(a: A) -> Option<B> {
if TypeId::of::<B>() == a.type_id() {
// SAFETY: Just checked we have the right type.
Some(unsafe_cast(a))
} else {
None
}
}
/// Cast a Boxed type into another type.
#[inline(always)]
#[must_use]
pub fn unsafe_cast_box<X: Any, T: Any>(item: Box<X>) -> Option<T> {
// Only allow casting to the exact same type
if TypeId::of::<X>() == TypeId::of::<T>() {
// SAFETY: just checked whether we are pointing to the correct type
unsafe {
let raw: *mut dyn Any = Box::into_raw(item as Box<dyn Any>);
Some(*Box::from_raw(raw as *mut T))
}
} else {
None
}
}