blob: 9c5d50e52675c3e304c191bef573843a8bf117d7 [file] [edit]
// Copyright 2024 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef V8_OBJECTS_CASTING_H_
#define V8_OBJECTS_CASTING_H_
/**
* V8 Casting Guide
* ================
*
* This file defines the casting infrastructure for V8's tagged objects.
*
* Untrusted objects (e.g. JSObject) live in the sandbox and their Map can be
* tampered with. Trusted objects (e.g. BytecodeArray) live outside the
* sandbox in trusted space.
*
*
* 1. Release-Time Security Checks (Always-on CHECK or SBXCHECK)
* -------------------------------------------------------------
* These are required when type safety must be guaranteed at runtime under
* the V8 Sandbox attacker model.
*
* To | Untrusted Trusted
* From |
* --------------------------------------------------
* Object | CheckedCast <Impossible>*
* TrustedObject | - SbxCast
*
* * Note: A checked cast from a generic untrusted Object to a TrustedObject is
* impossible at runtime because untrusted pointers can point to arbitrary,
* unaligned memory, making a sound runtime type check impossible.
*
*
* 2. Debug-Only Validation Checks (DCHECK-only or no-op)
* ------------------------------------------------------
* These are used when type safety is statically guaranteed, validated via
* non-typesystem invariants, or when performance is critical.
*
* To | Untrusted Trusted
* From |
* --------------------------------------------------
* Object | Cast TrustedCast
* TrustedObject | - TrustedCast
* Uninitialized | UncheckedCast -
*
* * Note: "Uninitialized" refers to partially-initialized objects (e.g., during
* deserialization or construction) where CastTraits checks would fail.
*
*
* Guide to Cast Functions
* =======================
*
* Cast<T>
* -------
* The default cast for untrusted objects. Only performs a debug-only DCHECK.
* Since untrusted objects can be tampered with anyway, release-time checks
* provide no additional security.
*
* TrustedCast<T>
* --------------
* Use this for trusted objects when the type is known through external means
* but cannot be proven locally to the type system (e.g., trusted stack slots).
* Like Cast<T>, it only performs a debug-only DCHECK.
*
* WARNING: Never use TrustedCast<T> on pointers loaded from untrusted/sandbox-
* accessible fields, as an attacker can tamper with them and cause release-time
* type confusion. Use SbxCast<T> or TryCast<T> instead.
*
* SbxCast<T>
* ----------
* Use this when a trusted pointer is obtained from an untrusted source AND the
* TPT (Trusted Pointer Table) tag is shared among multiple types (1:N mapping).
* It performs an always-on SBXCHECK to prevent type-confusion attacks.
*
* CheckedCast<T>
* --------------
* Performs an always-on CHECK at release time. Use this when casting untrusted
* objects where correctness is absolutely critical.
*
* TryCast<T>
* ----------
* Performs a dynamic type check and returns a boolean. For trusted objects,
* this is specifically used for 1:N mappings where multiple instance types
* share the same TPT tag and you need to safely distinguish them at runtime.
*
* Note on 1:1 Mappings: For trusted objects with a 1:1 mapping between their
* InstanceType and their TPT tag (like BytecodeArray), no explicit cast is
* usually needed when reading from a field, as ReadTrustedPointerField<T>
* already ensures the correct type.
*
* UncheckedCast<T>
* ----------------
* Performs a raw cast with zero compile-time or runtime checks. Used when the
* object is partially initialized (so Is<T> checks would fail), or when the
* type is already proven.
*
* GCSafeCast<T>
* -------------
* Performs a debug-only GC-aware type check. Used only within garbage
* collection code paths.
*/
#include <type_traits>
#include "include/v8-source-location.h"
#include "src/base/logging.h"
#include "src/objects/tagged.h"
#include "src/sandbox/check.h"
namespace v8::internal {
// CastTraits<T> is a type trait that defines type checking behaviour for
// tagged object casting. The expected specialization is:
//
// template<>
// struct CastTraits<SomeObject> {
// template<typename From>
// static bool AllowFrom(Tagged<From> value) {
// return IsSomeObject(value);
// }
// };
//
// or, likely, just specializations of AllowFrom for Object and HeapObject,
// under the assumption that the HeapObject implementation is the same for all
// HeapObjects and the Object implementation has additional overhead in Smi
// checks.
//
// struct CastTraits<Object> {
// static bool AllowFrom(Tagged<HeapObject> value) {
// return IsSomeObject(value);
// }
// static bool AllowFrom(Tagged<Object> value) {
// return IsSomeObject(value);
// }
// };
//
template <typename To>
struct CastTraits;
class Oddball;
template <typename T>
concept NotGCedType = !std::is_base_of_v<HeapObject, T>;
// `Is<T>(value)` checks whether `value` is a tagged object of type `T`.
template <typename T, typename U>
inline bool Is(Tagged<U> value) {
return CastTraits<T>::AllowFrom(value);
}
template <typename T>
inline bool Is(const HeapObject* obj) {
return Is<T>(Tagged<HeapObject>(obj));
}
template <typename T, typename U>
inline bool Is(IndirectHandle<U> value);
template <typename T, typename U>
inline bool Is(DirectHandle<U> value);
// Is<T> checks for MaybeHandles are forbidden -- you need to first convert to
// a Handle with ToHandle or ToHandleChecked.
template <typename T, typename U>
bool Is(MaybeIndirectHandle<U> value) = delete;
template <typename T, typename U>
bool Is(MaybeDirectHandle<U> value) = delete;
// C++ versions for `Is<T>` that accept pointers and references.
template <typename To, typename From>
requires NotGCedType<To>
bool Is(const From& value) {
return CastTraits<std::remove_const_t<To>>::AllowFrom(value);
}
template <typename To, typename From>
requires NotGCedType<To>
bool Is(From& value) {
return Is<To>(const_cast<const From&>(value));
}
template <typename To, typename From>
requires NotGCedType<To>
bool Is(const From* value) {
return value && Is<To>(*value);
}
template <typename To, typename From>
requires NotGCedType<To>
bool Is(From* value) {
return value && Is<To>(*static_cast<const From*>(value));
}
// `UncheckedCast<T>(value)` casts `value` to a tagged object of type `T`,
// without checking the type of value.
template <typename To, typename From>
inline Tagged<To> UncheckedCast(Tagged<From> value) {
return Tagged<To>(value.ptr());
}
template <typename To, typename From>
inline IndirectHandle<To> UncheckedCast(IndirectHandle<From> value);
template <typename To, typename From>
inline MaybeIndirectHandle<To> UncheckedCast(MaybeIndirectHandle<From> value);
template <typename To, typename From>
inline DirectHandle<To> UncheckedCast(DirectHandle<From> value);
template <typename To, typename From>
inline MaybeDirectHandle<To> UncheckedCast(MaybeDirectHandle<From> value);
// HasCastImplementation is a concept that checks for the existence of
// Is<To>(Holder<From>) and UncheckedCast<To>(Holder<From>).
template <template <typename> class Holder, typename To, typename From>
concept HasTryCastImplementation = requires(Holder<From> value) {
{ Is<To>(value) } -> std::same_as<bool>;
{ UncheckedCast<To>(value) } -> std::same_as<Holder<To>>;
};
template <typename Derived, typename Base>
concept HasCppRefTryCastImplementation =
NotGCedType<Derived> && requires(Base& value) {
{ Is<Derived>(value) } -> std::same_as<bool>;
};
template <typename Derived, typename Base>
concept HasCppPointerTryCastImplementation =
NotGCedType<Derived> && requires(Base* value) {
{ Is<Derived>(value) } -> std::same_as<bool>;
};
// `TryCast<T>(value, &out)` casts `value` to a tagged object of type `T` and
// writes the value to `out`, returning true if the cast succeeded and false if
// it failed.
template <typename To, typename From, template <typename> class Holder>
requires HasTryCastImplementation<Holder, To, From>
inline bool TryCast(Holder<From> value, Holder<To>* out) {
static_assert(!is_subtype_v<To, TrustedObject> ||
is_subtype_v<From, Union<Smi, Oddball, TrustedObject>>,
"Type of untrusted objects cannot be checked reliably");
if (!Is<To>(value)) return false;
*out = UncheckedCast<To>(value);
return true;
}
template <typename Derived, typename Base>
requires HasCppRefTryCastImplementation<Derived, Base>
inline bool TryCast(Base& value, Derived** out) {
if (!Is<Derived>(value)) return false;
*out = static_cast<Derived*>(&value);
return true;
}
template <typename Derived, typename Base>
requires HasCppPointerTryCastImplementation<Derived, Base>
inline bool TryCast(Base* value, Derived** out) {
if (!Is<Derived>(value)) return false;
*out = static_cast<Derived*>(value);
return true;
}
#ifdef DEBUG
template <typename T>
bool GCAwareObjectTypeCheck(Tagged<Object> object, const Heap* heap);
#endif // DEBUG
// `GCSafeCast<T>(value)` casts `object` to a tagged object of type `T` and
// should be used when the cast can be called from within a GC. The case all
// includes a debug check that `object` is either a tagged object of type `T`,
// or one of few special cases possible during GC (see GCAwareObjectTypeCheck):
// 1) `object` was already evacuated and the forwarding address refers to a
// tagged object of type `T`.
// 2) During Scavenger, `object` is a large object.
// 3) During a conservative Scavenger, `object` is a pinned object.
template <typename T>
Tagged<T> GCSafeCast(Tagged<Object> object, const Heap* heap) {
DCHECK(GCAwareObjectTypeCheck<T>(object, heap));
return UncheckedCast<T>(object);
}
// Check if a value holder is null -- used for Cast DCHECKs, so that null
// handles pass the check.
template <typename T, typename U>
inline bool NullOrIs(Tagged<U> value) {
// Raw tagged values are not allowed to be null, just check Is<T>.
return Is<T>(value);
}
template <typename T, typename U>
inline bool NullOrIs(IndirectHandle<U> value) {
return value.is_null() || Is<T>(value);
}
template <typename T, typename U>
inline bool NullOrIs(DirectHandle<U> value) {
return value.is_null() || Is<T>(value);
}
template <typename T, typename U>
inline bool NullOrIs(MaybeIndirectHandle<U> value) {
IndirectHandle<U> handle;
return !value.ToHandle(&handle) || Is<T>(handle);
}
template <typename T, typename U>
inline bool NullOrIs(MaybeDirectHandle<U> value) {
DirectHandle<U> handle;
return !value.ToHandle(&handle) || Is<T>(handle);
}
template <typename Derived, typename Base>
bool NullOrIs(Base& value) {
return Is<Derived>(value);
}
template <typename Derived, typename Base>
bool NullOrIs(Base* value) {
return !value || Is<Derived>(*value);
}
// HasCastImplementation is a concept that checks for the existence of
// NullOrIs<To>(Holder<From>) and UncheckedCast<To>(Holder<From>).
template <template <typename> class Holder, typename To, typename From>
concept HasCastImplementation = requires(Holder<From> value) {
{ NullOrIs<To>(value) } -> std::same_as<bool>;
{ UncheckedCast<To>(value) } -> std::same_as<Holder<To>>;
};
template <typename Derived, typename Base>
concept HasCppRefCastImplementation =
NotGCedType<Derived> && requires(Base& value) {
{ Is<Derived>(value) } -> std::same_as<bool>;
};
template <typename Derived, typename Base>
concept HasCppPointerCastImplementation =
NotGCedType<Derived> && requires(Base* value) {
{ Is<Derived>(value) } -> std::same_as<bool>;
};
// `TrustedCast<T>(value)` casts `value` to a tagged object of type `T`, only
// doing a debug check that `value` is a tagged object of type `T`. Down-casts
// to trusted objects are allowed for callers which already ensured their
// correctness. Null-valued holders are allowed.
//
// WARNING: This must NEVER be used on pointers loaded from untrusted/sandbox-
// accessible fields, as an attacker can tamper with them and cause release-time
// type confusion. Use SbxCast<T> or TryCast<T> instead for those cases.
template <typename To, typename From, template <typename> class Holder>
requires HasCastImplementation<Holder, To, From>
inline Holder<To> TrustedCast(
Holder<From> value, SourceLocation loc = SourceLocation::CurrentIfDebug()) {
// Casting pointers to in-sandbox objects is allowed even with an active
// DisallowSandboxAccess scope (as casting itself doesn't access any memory).
// However, in debug builds the DCHECK below will need to access the Map of
// the object, so we need to temporarily enable sandbox access. If no
// DisallowSandboxAccess scope is active, this is a no-op.
DCHECK_WITH_SANDBOX_ACCESS_AND_MSG_AND_LOC(
NullOrIs<To>(value), V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
return UncheckedCast<To>(value);
}
// `SbxCast<T>(value)` casts `value` to a tagged object of trusted type `T`,
// with a sandbox-only dynamic type check ensuring that `value` is a tagged
// object of type `T`. This is required for security when the object was
// obtained from an untrusted source AND the TPT tag is shared among multiple
// types (1:N mapping). Null-valued holders are allowed.
template <typename To, typename From, template <typename> class Holder>
requires HasCastImplementation<Holder, To, From>
inline Holder<To> SbxCast(
Holder<From> value, SourceLocation loc = SourceLocation::CurrentIfDebug()) {
// Under the attacker model of the sandbox we cannot trust the type of
// in-sandbox objects for sandbox checks as these objects can be arbitrarily
// tampered with.
static_assert(is_subtype_v<To, TrustedObject>);
static_assert(is_subtype_v<From, Union<Smi, Oddball, TrustedObject>>,
"Type of untrusted objects cannot be checked reliably");
DCHECK_WITH_MSG_AND_LOC(NullOrIs<To>(value),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
SBXCHECK(NullOrIs<To>(value));
return UncheckedCast<To>(value);
}
template <typename Derived, typename Base>
requires HasCppRefCastImplementation<Derived, Base>
inline Derived& SbxCast(Base& from,
SourceLocation loc = SourceLocation::CurrentIfDebug()) {
DCHECK_WITH_MSG_AND_LOC(NullOrIs<Derived>(from),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
SBXCHECK(NullOrIs<Derived>(from));
return static_cast<Derived&>(from);
}
template <typename Derived, typename Base>
requires HasCppPointerCastImplementation<Derived, Base>
inline Derived* SbxCast(Base* from,
SourceLocation loc = SourceLocation::CurrentIfDebug()) {
DCHECK_WITH_MSG_AND_LOC(NullOrIs<Derived>(from),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
SBXCHECK(NullOrIs<Derived>(from));
return static_cast<Derived*>(from);
}
// `CheckedCast<T>(value)` casts `value` to a tagged object of type `T`,
// with a always-on dynamic type check ensuring that `value` is a tagged object
// of type `T`. Null-valued holders are allowed.
template <typename To, typename From, template <typename> class Holder>
requires HasCastImplementation<Holder, To, From>
inline Holder<To> CheckedCast(
Holder<From> value, SourceLocation loc = SourceLocation::CurrentIfDebug()) {
DCHECK_WITH_MSG_AND_LOC(NullOrIs<To>(value),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
CHECK(NullOrIs<To>(value));
return UncheckedCast<To>(value);
}
template <typename Derived, typename Base>
requires HasCppRefCastImplementation<Derived, Base>
inline Derived& CheckedCast(
Base& from, SourceLocation loc = SourceLocation::CurrentIfDebug()) {
DCHECK_WITH_MSG_AND_LOC(NullOrIs<Derived>(from),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
CHECK(NullOrIs<Derived>(from));
return static_cast<Derived&>(from);
}
template <typename Derived, typename Base>
requires HasCppPointerCastImplementation<Derived, Base>
inline Derived* CheckedCast(
Base* from, SourceLocation loc = SourceLocation::CurrentIfDebug()) {
DCHECK_WITH_MSG_AND_LOC(NullOrIs<Derived>(from),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
CHECK(NullOrIs<Derived>(from));
return static_cast<Derived*>(from);
}
// `Cast<T>(value)` casts `value` to a tagged object of type `T`, with a debug
// check that `value` is a tagged object of type `T`. Null-valued holders are
// allowed. Down-casts to trusted objects are not allowed to prevent mistakes.
template <typename To, typename From, template <typename> class Holder>
requires HasCastImplementation<Holder, To, From>
inline Holder<To> Cast(Holder<From> value,
SourceLocation loc = SourceLocation::CurrentIfDebug()) {
static_assert(
!is_subtype_v<To, TrustedObject> || is_subtype_v<From, To>,
"Down-casting to trusted objects is verboten. Pick one of:\n"
"* TryCast if the use is guarded by a test\n"
" if (Trusted t; TryCast(obj, &t)) { t->foo(); }\n"
"* (Checked|Sbx)Cast if the object is expected to be of a certain type\n"
" Tagged<Trusted> t = SbxCast<Trusted>(obj);\n"
"* TrustedCast if correct by construction (or other deprecated usage)\n"
" Tagged<Trusted> t = "
"TrustedCast<Trusted>(TrustedPointerField::ReadTrustedPointerField<"
"kTrustedTag>(obj, ...));\n");
return TrustedCast<To>(value, loc);
}
// TODO(leszeks): Figure out a way to make these cast to actual pointers rather
// than Tagged.
template <typename To, typename From>
requires std::is_base_of_v<HeapObject, From>
inline Tagged<To> UncheckedCast(const From* value) {
return UncheckedCast<To>(Tagged(value));
}
template <typename To, typename From>
requires std::is_base_of_v<HeapObject, From>
inline Tagged<To> TrustedCast(const From* value) {
return TrustedCast<To>(Tagged(value));
}
template <typename To, typename From>
requires std::is_base_of_v<HeapObject, From>
inline Tagged<To> CheckedCast(const From* value) {
return CheckedCast<To>(Tagged(value));
}
template <typename To, typename From>
requires std::is_base_of_v<HeapObject, From>
inline Tagged<To> SbxCast(const From* value) {
return SbxCast<To>(Tagged(value));
}
template <typename To, typename From>
requires std::is_base_of_v<HeapObject, From>
inline Tagged<To> Cast(const From* value,
SourceLocation loc = SourceLocation::CurrentIfDebug()) {
return Cast<To>(Tagged(value), loc);
}
template <typename Derived, typename Base>
requires HasCppRefCastImplementation<Derived, Base>
inline Derived& Cast(Base& from,
SourceLocation loc = SourceLocation::CurrentIfDebug()) {
DCHECK_WITH_MSG_AND_LOC(NullOrIs<Derived>(from),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
return static_cast<Derived&>(from);
}
template <typename Derived, typename Base>
requires HasCppPointerCastImplementation<Derived, Base>
inline Derived* Cast(Base* from,
SourceLocation loc = SourceLocation::CurrentIfDebug()) {
DCHECK_WITH_MSG_AND_LOC(NullOrIs<Derived>(from),
V8_PRETTY_FUNCTION_VALUE_OR("Cast type check"), loc);
return static_cast<Derived*>(from);
}
// `Is<T>(maybe_weak_value)` specialization for possible weak values and strong
// target `T`, that additionally first checks whether `maybe_weak_value` is
// actually a strong value (or a Smi, which can't be weak).
template <typename T, typename U>
inline bool Is(Tagged<MaybeWeak<U>> value) {
// Cast from maybe weak to strong needs to be strong or smi.
if constexpr (!is_maybe_weak_v<T>) {
if (!value.IsStrongOrSmi()) return false;
return CastTraits<T>::AllowFrom(Tagged<U>(value.ptr()));
} else {
// Dispatches to CastTraits<MaybeWeak<T>> below.
return CastTraits<T>::AllowFrom(value);
}
}
template <typename T, typename... U>
constexpr inline bool Is(Tagged<Union<U...>> value) {
using UnionU = Union<U...>;
if constexpr (is_weak_v<UnionU>) {
return Is<T>(Tagged<Weak<HeapObject>>(value));
} else if constexpr (is_maybe_weak_v<UnionU>) {
// Cleared values are always ok.
if (value.IsCleared()) return true;
// TODO(leszeks): Skip Smi check for values that are known to not be Smi.
if (value.IsSmi()) {
return Is<T>(Tagged<Smi>(value.ptr()));
}
return Is<T>(MakeStrong(value));
} else if constexpr (is_subtype_v<UnionU, HeapObject>) {
return Is<T>(Tagged<HeapObject>(value));
} else {
static_assert(is_subtype_v<UnionU, Object>);
return Is<T>(Tagged<Object>(value));
}
}
// Specialization for weak cast targets, which first converts the incoming
// value to a strong reference and then checks if the cast to the strong T
// is allowed. Cleared weak references always return true.
template <typename T>
struct CastTraits<Weak<T>> {
template <typename U>
static bool AllowFrom(Tagged<U> value) {
if constexpr (is_weak_v<U>) {
return CastTraits<T>::AllowFrom(MakeStrong(value));
}
if constexpr (is_maybe_weak_v<U>) {
// Cleared values are always ok.
if (value.IsCleared()) return true;
// TODO(leszeks): Skip Smi check for values that are known to not be Smi.
if (value.IsSmi()) return false;
return CastTraits<T>::AllowFrom(MakeStrong(value));
} else {
// Can't cast weak to non-weak.
return false;
}
}
};
template <typename... T>
struct CastTraits<Union<T...>> {
static inline bool AllowFrom(Tagged<Object> value) {
return (Is<T>(value) || ...);
}
static inline bool AllowFrom(Tagged<HeapObject> value) {
return (Is<T>(value) || ...);
}
};
template <>
struct CastTraits<Object> {
static inline bool AllowFrom(Tagged<Object> value) { return true; }
};
template <>
struct CastTraits<Smi> {
static inline bool AllowFrom(Tagged<Object> value) { return value.IsSmi(); }
static inline bool AllowFrom(Tagged<HeapObject> value) { return false; }
};
template <>
struct CastTraits<HeapObject> {
static inline bool AllowFrom(Tagged<Object> value) {
return value.IsHeapObject();
}
static inline bool AllowFrom(Tagged<HeapObject> value) { return true; }
};
} // namespace v8::internal
#endif // V8_OBJECTS_CASTING_H_