CVE-2026-9521
Description
A security vulnerability has been detected in fraillt bitsery up to 5.2.4. Affected is the function loadFromSharedState in the library include/bitsery/ext/std_smart_ptr.h. Such manipulation leads to improper validation of specified type of input. It is possible to launch the attack remotely. The exploit has been disclosed publicly and may be used. Upgrading to version 5.2.5 is able to address this issue. The name of the patch is 66d16516e24893bebc1c8af52bf2fe9ad0735061. Upgrading the affected component is advised.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Type confusion in bitsery's shared pointer deserialization allows remote attackers to achieve arbitrary code execution via crafted payload.
Vulnerability
A type confusion vulnerability exists in bitsery versions up to and including 5.2.4. The flaw resides in the loadFromSharedState function within include/bitsery/ext/std_smart_ptr.h. During deserialization of shared pointers, the library fails to properly validate the target type, allowing a crafted payload to assign a shared pointer to an incompatible type [1][2].
Exploitation
An attacker can exploit this vulnerability remotely by providing a malicious serialized archive to an application using bitsery to deserialize untrusted data. The exploit does not require authentication and is publicly documented with proof-of-concept examples demonstrating address leakage, memory read, and vtable hijacking [3].
Impact
Successful exploitation can lead to sensitive information disclosure (ASLR bypass), arbitrary memory read, denial-of-service, and potentially arbitrary code execution through control flow hijacking [3]. The impact is severe, as it can compromise the confidentiality, integrity, and availability of the affected system.
Mitigation
The vulnerability is fixed in bitsery version 5.2.5, released on 2025-10-09 [1]. The fix is identified in commit 66d16516e24893bebc1c8af52bf2fe9ad0735061 [2]. Users should upgrade to version 5.2.5 or later. No workarounds have been disclosed.
AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
166d16516e248Fix shared polymorphic poiner like type serialization/deserialization
5 files changed · +391 −101
include/bitsery/ext/pointer.h+23 −45 modified@@ -104,7 +104,7 @@ struct PtrObserverManager static void destroyPolymorphic(T& obj, MemResourceBase*, - PolymorphicHandlerBase&) + const std::shared_ptr<PolymorphicHandlerBase>&) { obj = nullptr; } @@ -130,47 +130,22 @@ struct NonPtrManager static void create(T&, MemResourceBase*, size_t) {} - static void createPolymorphic(T&, MemResourceBase*, PolymorphicHandlerBase&) + static void createPolymorphic(T&, + MemResourceBase*, + const std::shared_ptr<PolymorphicHandlerBase>&) { } static void destroy(T&, MemResourceBase*, size_t) {} - static void destroyPolymorphic(T&, MemResourceBase*, PolymorphicHandlerBase&) + static void destroyPolymorphic(T&, + MemResourceBase*, + const std::shared_ptr<PolymorphicHandlerBase>&) { } // LCOV_EXCL_STOP }; -// this class is used by NonPtrManager -struct NoRTTI -{ - template<typename TBase> - static size_t get(TBase&) - { - return 0; - } - - template<typename TBase> - static constexpr size_t get() - { - return 0; - } - - template<typename TBase, typename TDerived> - static constexpr TDerived* cast(TBase* obj) - { - static_assert(!std::is_pointer<TDerived>::value, ""); - return dynamic_cast<TDerived*>(obj); - } - - template<typename TBase> - static constexpr bool isPolymorphic() - { - return false; - } -}; - } template<typename RTTI> @@ -179,31 +154,34 @@ using PointerOwnerBase = PolymorphicContext, RTTI>; -using PointerOwner = PointerOwnerBase<StandardRTTI>; - -using PointerObserver = +template<typename RTTI> +using PointerObserverBase = pointer_utils::PointerObjectExtensionBase<pointer_details::PtrObserverManager, PolymorphicContext, - pointer_details::NoRTTI>; + RTTI>; // inherit from PointerObjectExtensionBase in order to specify // PointerType::NotNull -class ReferencedByPointer +template<typename RTTI> +class ReferencedByPointerBase : public pointer_utils::PointerObjectExtensionBase< pointer_details::NonPtrManager, PolymorphicContext, - pointer_details::NoRTTI> + RTTI> { public: - ReferencedByPointer() + ReferencedByPointerBase() : pointer_utils::PointerObjectExtensionBase<pointer_details::NonPtrManager, PolymorphicContext, - pointer_details::NoRTTI>( - PointerType::NotNull) + RTTI>(PointerType::NotNull) { } }; +using PointerOwner = PointerOwnerBase<StandardRTTI>; +using PointerObserver = PointerObserverBase<StandardRTTI>; +using ReferencedByPointer = ReferencedByPointerBase<StandardRTTI>; + } namespace traits { @@ -219,8 +197,8 @@ struct ExtensionTraits<ext::PointerOwnerBase<RTTI>, T*> !RTTI::template isPolymorphic<TValue>(); }; -template<typename T> -struct ExtensionTraits<ext::PointerObserver, T*> +template<typename T, typename RTTI> +struct ExtensionTraits<ext::PointerObserverBase<RTTI>, T*> { // although pointer observer doesn't serialize anything, but we still add // value overload support to be consistent with pointer owners observer only @@ -231,8 +209,8 @@ struct ExtensionTraits<ext::PointerObserver, T*> static constexpr bool SupportLambdaOverload = false; }; -template<typename T> -struct ExtensionTraits<ext::ReferencedByPointer, T> +template<typename T, typename RTTI> +struct ExtensionTraits<ext::ReferencedByPointerBase<RTTI>, T> { // allow everything, because it is serialized as regular type, except it also // creates pointerId that is required by NonOwningPointer to work
include/bitsery/ext/std_smart_ptr.h+18 −1 modified@@ -198,10 +198,27 @@ struct SmartPtrOwnerManager state.obj = std::shared_ptr<TElement>(obj); } + static void saveToSharedStatePolymorphic(TSharedState& state, T& obj) + { + state.obj = std::shared_ptr<TElement>(obj); + } + static void loadFromSharedState(TSharedState& state, T& obj) { // reinterpret_pointer_cast is only since c++17 - auto p = reinterpret_cast<TElement*>(state.obj.get()); + auto v = state.obj.get(); + auto p = reinterpret_cast<TElement*>(v); + obj = std::shared_ptr<TElement>(state.obj, p); + } + + static void loadFromSharedStatePolymorphic(TSharedState& state, T& obj) + { + // TODO Fix pointer addresses in case objects are deserialized using + // different bases + + // reinterpret_pointer_cast is only since c++17 + auto v = state.obj.get(); + auto p = reinterpret_cast<TElement*>(v); obj = std::shared_ptr<TElement>(state.obj, p); } };
include/bitsery/ext/utils/pointer_utils.h+75 −18 modified@@ -120,10 +120,12 @@ struct PLCInfoSerializer : PLCInfo struct PLCInfoDeserializer : PLCInfo { PLCInfoDeserializer(void* ptr, + size_t sharedTypeId_, PointerOwnershipType ownershipType_, MemResourceBase* memResource_) : PLCInfo(ownershipType_) , ownerPtr{ ptr } + , sharedTypeId{ sharedTypeId_ } , memResource{ memResource_ } , observersList{ StdPolyAlloc<std::reference_wrapper<void*>>{ memResource_ } } {}; @@ -157,6 +159,9 @@ struct PLCInfoDeserializer : PLCInfo } void* ownerPtr; + // used for polymorphic types in order to identify + // if shared objects can be assigned + size_t sharedTypeId; MemResourceBase* memResource; std::vector<std::reference_wrapper<void*>, StdPolyAlloc<std::reference_wrapper<void*>>> @@ -254,8 +259,8 @@ class PointerLinkingContextDeserialization PLCInfoDeserializer& getInfoById(size_t id, PointerOwnershipType ptrType) { - auto res = - _idMap.emplace(id, PLCInfoDeserializer{ nullptr, ptrType, _memResource }); + auto res = _idMap.emplace( + id, PLCInfoDeserializer{ nullptr, 0, ptrType, _memResource }); auto& ptrInfo = res.first->second; if (!res.second) ptrInfo.update(ptrType); @@ -352,8 +357,8 @@ class PointerObjectExtensionBase if (ptr) { auto& ctx = ser.template context< pointer_utils::PointerLinkingContextSerialization>(); - auto& ptrInfo = - ctx.getInfoByPtr(getBasePtr(ptr), TPtrManager<T>::getOwnership()); + auto& ptrInfo = ctx.getInfoByPtr(getRootPtr(ser, ptr, IsPolymorphic<T>{}), + TPtrManager<T>::getOwnership()); details::writeSize(ser.adapter(), ptrInfo.id); if (TPtrManager<T>::getOwnership() != PointerOwnershipType::Observer) { if (!ptrInfo.isSharedProcessed) @@ -411,7 +416,7 @@ class PointerObjectExtensionBase const auto& ctx = des.template context<TPolymorphicContext<RTTI>>(); auto ptr = TPtrManager<TObj>::getPtr(obj); TPtrManager<TObj>::destroyPolymorphic( - obj, memResource, ctx.getPolymorphicHandler(*ptr)); + obj, memResource, ctx.getPolymorphicHandler(ptr)); } template<typename Des, typename TObj> @@ -426,16 +431,23 @@ class PointerObjectExtensionBase RTTI::template get<typename TPtrManager<TObj>::TElement>()); } - template<typename T> - const void* getBasePtr(const T* ptr) const + template<typename Ser, typename T> + const void* getRootPtr(Ser&, const T* ptr, std::false_type) const { - // todo implement handling of types with virtual inheritance - // this is required to correctly track same object, when one object is - // derived and other is base class e.g. shared_ptr<Base> and - // weak_ptr<Derived> or pointer observer Base* return ptr; } + // same pointer can be accessed through different types with the same base + // e.g. if we have Base class and Derived(Base) we'll get different pointer + // address depending if we access it through Base or Derived + // this function always returns "root" (Base) pointer. + template<typename Ser, typename T> + const void* getRootPtr(Ser& ser, const T* ptr, std::true_type) const + { + const auto& ctx = ser.template context<TPolymorphicContext<RTTI>>(); + return ctx.getPolymorphicHandler(ptr)->getRootPtr(ptr); + } + template<typename Ser, typename TPtr, typename Fnc> void serializeImpl(Ser& ser, TPtr& ptr, Fnc&&, std::true_type) const { @@ -506,8 +518,11 @@ class PointerObjectExtensionBase std::true_type, OwnershipType<PointerOwnershipType::SharedOwner>) const { + const auto& ctx = des.template context<TPolymorphicContext<RTTI>>(); + const size_t baseTypeId = + RTTI::template get<typename TPtrManager<T>::TElement>(); + size_t deserializedTypeId = 0; if (!ptrInfo.sharedState) { - const auto& ctx = des.template context<TPolymorphicContext<RTTI>>(); ctx.deserialize( des, TPtrManager<T>::getPtr(obj), @@ -521,12 +536,46 @@ class PointerObjectExtensionBase memResource](const std::shared_ptr<PolymorphicHandlerBase>& handler) { TPtrManager<T>::destroyPolymorphic(obj, memResource, handler); }); - if (!ptrInfo.sharedState) - TPtrManager<T>::saveToSharedState( + if (!ptrInfo.sharedState) { + TPtrManager<T>::saveToSharedStatePolymorphic( createAndGetSharedStateObj<T>(ptrInfo), obj); + } + ptrInfo.sharedTypeId = + ctx.getPolymorphicHandler(TPtrManager<T>::getPtr(obj)) + ->getDerivedTypeId(); + // since we just deserialized an object, we can skip checking hierarchy + // chain by assigning baseType id instead of derived type id + deserializedTypeId = baseTypeId; + } else { + deserializedTypeId = ptrInfo.sharedTypeId; } - TPtrManager<T>::loadFromSharedState(getSharedStateObj<T>(ptrInfo), obj); - ptrInfo.processOwner(TPtrManager<T>::getPtr(obj)); + + if (canAssignToShared(baseTypeId, deserializedTypeId, ctx)) { + TPtrManager<T>::loadFromSharedStatePolymorphic( + getSharedStateObj<T>(ptrInfo), obj); + ptrInfo.processOwner(TPtrManager<T>::getPtr(obj)); + } else { + des.adapter().error(ReaderError::InvalidPointer); + } + } + + // check if actual deserialized type can be assigned to the base type + // (statically typed) + bool canAssignToShared(size_t baseTypeId, + size_t deserializedTypeId, + const TPolymorphicContext<RTTI>& ctx) const + { + if (baseTypeId == deserializedTypeId) + return true; + auto bases = ctx.getDirectBases(deserializedTypeId); + if (bases) { + for (auto typeId : *bases) { + if (canAssignToShared(baseTypeId, typeId, ctx)) { + return true; + } + } + } + return false; } template<typename Des, typename T, typename Fnc> @@ -538,6 +587,8 @@ class PointerObjectExtensionBase std::false_type, OwnershipType<PointerOwnershipType::SharedOwner>) const { + const size_t baseTypeId = + RTTI::template get<typename TPtrManager<T>::TElement>(); if (!ptrInfo.sharedState) { auto ptr = TPtrManager<T>::getPtr(obj); if (ptr) { @@ -552,9 +603,15 @@ class PointerObjectExtensionBase ptr = TPtrManager<T>::getPtr(obj); } fnc(des, *ptr); + ptrInfo.sharedTypeId = + RTTI::template get<typename TPtrManager<T>::TElement>(); + } + if (baseTypeId == ptrInfo.sharedTypeId) { + TPtrManager<T>::loadFromSharedState(getSharedStateObj<T>(ptrInfo), obj); + ptrInfo.processOwner(TPtrManager<T>::getPtr(obj)); + } else { + des.adapter().error(ReaderError::InvalidPointer); } - TPtrManager<T>::loadFromSharedState(getSharedStateObj<T>(ptrInfo), obj); - ptrInfo.processOwner(TPtrManager<T>::getPtr(obj)); } template<typename Des, typename T, typename Fnc, typename isPolymorph>
include/bitsery/ext/utils/polymorphism_utils.h+155 −37 modified@@ -74,10 +74,18 @@ class PolymorphicHandlerBase virtual void process(void* ser, void* obj) const = 0; + virtual void* getRootPtr(const void* obj) const = 0; + + virtual size_t getDerivedTypeId() const = 0; + virtual ~PolymorphicHandlerBase() = default; }; -template<typename RTTI, typename TSerializer, typename TBase, typename TDerived> +template<typename RTTI, + typename TSerializer, + typename TRoot, + typename TBase, + typename TDerived> class PolymorphicHandler : public PolymorphicHandlerBase { public: @@ -97,6 +105,17 @@ class PolymorphicHandler : public PolymorphicHandlerBase static_cast<TSerializer*>(ser)->object(*fromBase(obj)); } + void* getRootPtr(const void* obj) const final + { + return RTTI::template cast<TBase, TRoot>( + static_cast<TBase*>(const_cast<void*>(obj))); + } + + size_t getDerivedTypeId() const final + { + return RTTI::template get<TDerived>(); + } + private: TDerived* fromBase(void* obj) const { @@ -109,6 +128,35 @@ class PolymorphicHandler : public PolymorphicHandlerBase } }; +// Even though we don't serialize/deserialize abstract classes +// object might still be accessed through abstract class, hence we need this +// for type information +template<typename RTTI, typename TRoot, typename TBase, typename TDerived> +class AbstractPolymorphicHandler : public PolymorphicHandlerBase +{ +public: + void* create(const pointer_utils::PolyAllocWithTypeId& alloc) const + { + assert(false); + return nullptr; + } + + void destroy(const pointer_utils::PolyAllocWithTypeId& alloc, void* ptr) const + { + assert(false); + }; + + void process(void* ser, void* obj) const { assert(false); } + + void* getRootPtr(const void* obj) const + { + assert(false); + return nullptr; + } + + size_t getDerivedTypeId() const { return RTTI::template get<TDerived>(); }; +}; + template<typename RTTI> class PolymorphicContext { @@ -137,47 +185,54 @@ class PolymorphicContext template<typename TSerializer, template<typename> class THierarchy, + typename TRoot, typename TBase, typename TDerived> - void add() + void add(size_t depth) { - addToMap<TSerializer, TBase, TDerived>(std::is_abstract<TDerived>{}); - addChilds<TSerializer, THierarchy, TBase, TDerived>( - typename THierarchy<TDerived>::Childs{}); + addToMap<TSerializer, TRoot, TBase, TDerived>(depth == 1, + std::is_abstract<TDerived>{}); + addChilds<TSerializer, THierarchy, TRoot, TBase, TDerived>( + depth + 1, typename THierarchy<TDerived>::Childs{}); } template<typename TSerializer, template<typename> class THierarchy, + typename TRoot, typename TBase, typename TDerived, typename T1, typename... Tn> - void addChilds(PolymorphicClassesList<T1, Tn...>) + void addChilds(size_t depth, PolymorphicClassesList<T1, Tn...>) { static_assert(std::is_base_of<TDerived, T1>::value, "PolymorphicBaseClass<TBase> must derive a list of derived " "classes from TBase."); - add<TSerializer, THierarchy, TBase, T1>(); - addChilds<TSerializer, THierarchy, TBase, TDerived>( - PolymorphicClassesList<Tn...>{}); - // iterate through derived class hierarchy as well - add<TSerializer, THierarchy, T1, T1>(); + add<TSerializer, THierarchy, TRoot, TBase, T1>(depth); + addChilds<TSerializer, THierarchy, TRoot, TBase, TDerived>( + depth, PolymorphicClassesList<Tn...>{}); + add<TSerializer, THierarchy, TRoot, T1, T1>(0); } template<typename TSerializer, template<typename> class THierarchy, + typename TRoot, typename TBase, typename TDerived> - void addChilds(PolymorphicClassesList<>) + void addChilds(size_t, PolymorphicClassesList<>) { } - template<typename TSerializer, typename TBase, typename TDerived> - void addToMap(std::false_type) + template<typename TSerializer, + typename TRoot, + typename TBase, + typename TDerived> + void addToMap(bool directBase, std::false_type) { - using THandler = PolymorphicHandler<RTTI, TSerializer, TBase, TDerived>; + using THandler = + PolymorphicHandler<RTTI, TSerializer, TRoot, TBase, TDerived>; BaseToDerivedKey key{ RTTI::template get<TBase>(), RTTI::template get<TDerived>() }; pointer_utils::StdPolyAlloc<THandler> alloc{ _memResource }; @@ -201,12 +256,51 @@ class PolymorphicContext } it->second.push_back(key.derivedHash); } + if (directBase) { + auto it = _derivedToBaseArray.find(key.derivedHash); + if (it == _derivedToBaseArray.end()) { + it = _derivedToBaseArray + .emplace(std::piecewise_construct, + std::forward_as_tuple(key.derivedHash), + std::forward_as_tuple( + pointer_utils::StdPolyAlloc<size_t>{ _memResource })) + .first; + } + it->second.push_back(key.baseHash); + } } - template<typename TSerializer, typename TBase, typename TDerived> - void addToMap(std::true_type) + template<typename TSerializer, + typename TRoot, + typename TBase, + typename TDerived> + void addToMap(bool directBase, std::true_type) { - // cannot add abstract class + using THandler = AbstractPolymorphicHandler<RTTI, TRoot, TBase, TDerived>; + BaseToDerivedKey key{ RTTI::template get<TBase>(), + RTTI::template get<TDerived>() }; + pointer_utils::StdPolyAlloc<THandler> alloc{ _memResource }; + auto ptr = alloc.allocate(1); + std::shared_ptr<THandler> handler( + new (ptr) THandler{}, + [alloc](THandler* data) mutable { + data->~THandler(); + alloc.deallocate(data, 1); + }, + alloc); + _baseToDerivedMap.emplace(key, std::move(handler)).second; + if (directBase) { + auto it = _derivedToBaseArray.find(key.derivedHash); + if (it == _derivedToBaseArray.end()) { + it = _derivedToBaseArray + .emplace(std::piecewise_construct, + std::forward_as_tuple(key.derivedHash), + std::forward_as_tuple( + pointer_utils::StdPolyAlloc<size_t>{ _memResource })) + .first; + } + it->second.push_back(key.baseHash); + } } MemResourceBase* _memResource; @@ -234,6 +328,17 @@ class PolymorphicContext std::vector<size_t, pointer_utils::StdPolyAlloc<size_t>>>>> _baseToDerivedArray; + // Used to iterate through hierarchy chain from most derived to the base(s) + std::unordered_map< + size_t, + std::vector<size_t, pointer_utils::StdPolyAlloc<size_t>>, + std::hash<size_t>, + std::equal_to<size_t>, + pointer_utils::StdPolyAlloc< + std::pair<const size_t, + std::vector<size_t, pointer_utils::StdPolyAlloc<size_t>>>>> + _derivedToBaseArray; + public: explicit PolymorphicContext(MemResourceBase* memResource = nullptr) : _memResource{ memResource } @@ -244,6 +349,11 @@ class PolymorphicContext std::pair<const size_t, std::vector<size_t, pointer_utils::StdPolyAlloc<size_t>>>>{ memResource } } + , _derivedToBaseArray{ pointer_utils::StdPolyAlloc< + std::pair<const size_t, + std::vector<size_t, pointer_utils::StdPolyAlloc<size_t>>>>{ + memResource } } + { } @@ -269,7 +379,7 @@ class PolymorphicContext typename... Tn> void registerBasesList(PolymorphicClassesList<T1, Tn...>) { - add<TSerializer, THierarchy, T1, T1>(); + add<TSerializer, THierarchy, T1, T1, T1>(0); registerBasesList<TSerializer, THierarchy>(PolymorphicClassesList<Tn...>{}); } @@ -278,18 +388,6 @@ class PolymorphicContext { } - // optional method, in case you want to construct base class hierarchy your - // self - template<typename TSerializer, typename TBase, typename TDerived> - void registerSingleBaseBranch() - { - static_assert(std::is_base_of<TBase, TDerived>::value, - "TDerived must be derived from TBase"); - static_assert(!std::is_abstract<TDerived>::value, - "TDerived cannot be abstract"); - addToMap<TSerializer, TBase, TDerived>(std::false_type{}); - } - template<typename Serializer, typename TBase> void serialize(Serializer& ser, TBase& obj) const { @@ -338,7 +436,7 @@ class PolymorphicContext // if object is null or different type, create new and assign it if (obj == nullptr || RTTI::template get<TBase>(*obj) != derivedHash) { if (obj) { - destroyFnc(getPolymorphicHandler(*obj)); + destroyFnc(getPolymorphicHandler(obj)); } obj = createFnc(handler); } @@ -349,12 +447,32 @@ class PolymorphicContext template<typename TBase> const std::shared_ptr<PolymorphicHandlerBase>& getPolymorphicHandler( - TBase& obj) const + TBase* obj) const { - auto deleteHandlerIt = _baseToDerivedMap.find(BaseToDerivedKey{ - RTTI::template get<TBase>(), RTTI::template get<TBase>(obj) }); - assert(deleteHandlerIt != _baseToDerivedMap.end()); - return deleteHandlerIt->second; + auto it = _baseToDerivedMap.find(BaseToDerivedKey{ + RTTI::template get<TBase>(), RTTI::template get<TBase>(*obj) }); + assert(it != _baseToDerivedMap.end()); + return it->second; + } + + template<typename TBase> + const std::shared_ptr<PolymorphicHandlerBase>& getPolymorphicHandler() const + { + auto it = _baseToDerivedMap.find(BaseToDerivedKey{ + RTTI::template get<TBase>(), RTTI::template get<TBase>() }); + assert(it != _baseToDerivedMap.end()); + return it->second; + } + + const std::vector<size_t, pointer_utils::StdPolyAlloc<size_t>>* + getDirectBases(size_t derivedTypeId) const + { + auto it = _derivedToBaseArray.find(derivedTypeId); + if (it != _derivedToBaseArray.end()) { + return &it->second; + } else { + return nullptr; + } } };
tests/serialization_ext_std_smart_ptr.cpp+120 −0 modified@@ -23,6 +23,8 @@ #include <bitsery/ext/inheritance.h> #include <bitsery/ext/pointer.h> #include <bitsery/ext/std_smart_ptr.h> +#include <bitsery/traits/string.h> +#include <bitsery/traits/vector.h> #include "serialization_test_utils.h" #include <gmock/gmock.h> @@ -77,6 +79,27 @@ serialize(S& s, Derived& o) s.value1b(o.y); } +struct DerivedSibling : virtual Base +{ + uint32_t y{}; + + DerivedSibling() = default; + + DerivedSibling(uint8_t x_, uint32_t y_) + { + x = x_; + y = y_; + } +}; + +template<typename S> +void +serialize(S& s, DerivedSibling& o) +{ + s.ext(o, VirtualBaseClass<Base>{}); + s.value4b(o.y); +} + struct MoreDerived : Derived { uint8_t z{}; @@ -741,6 +764,103 @@ TEST_F(SerializeExtensionStdSmartSharedPtr, EXPECT_THAT(baseRes1.use_count(), Eq(0)); } +struct MightMatchVecLayout +{ + size_t begin; + size_t end; +}; + +template<typename S> +void +serialize(S& s, MightMatchVecLayout& o) +{ + s.template value<sizeof(size_t)>(o.begin); + s.template value<sizeof(size_t)>(o.end); +} + +TEST_F(SerializeExtensionStdSmartSharedPtr, + NonPolymorphicObservedPointerMustMatchActuallyDeserializedObjectType) +{ + std::shared_ptr<std::vector<uint8_t>> baseData1{ new std::vector<uint8_t>{ + 'a', 'b', 'c' } }; + std::shared_ptr<MightMatchVecLayout> baseData2{ new MightMatchVecLayout{ + 1, 2 } }; + auto& ser = createSerializer(); + ser.ext(baseData1, StdSmartPtr{}, [](auto& ser, std::vector<uint8_t>& o) { + ser.container1b(o, 100); + }); + ser.ext(baseData2, StdSmartPtr{}); + // hack a buffer, so that during deserialization we would point to already + // deserialized object + sctx.buf[5] = 0x01; + + std::shared_ptr<std::vector<uint8_t>> baseRes1{}; + std::shared_ptr<MightMatchVecLayout> baseRes2{}; + auto& des = createDeserializer(); + des.ext(baseRes1, StdSmartPtr{}, [](auto& ser, std::vector<uint8_t>& o) { + ser.container1b(o, 100); + }); + // if we would blindly trust the input (that the object is already + // deserialized), we would end up + // using memory of that object. In this case, we will get internal + // representation of std::vector. + // If this object is available to attacker, he'll be able to see leaked + // address of that object, + // allowing him to bypass ASLR. + des.ext(baseRes2, StdSmartPtr{}); + + EXPECT_THAT(baseRes2.get(), ::testing::IsNull()); + auto err = des.adapter().error(); + EXPECT_THAT(err, Eq(bitsery::ReaderError::InvalidPointer)); + EXPECT_TRUE(isPointerContextValid()); +} + +TEST_F( + SerializeExtensionStdSmartSharedPtr, + PolymorphicObservedPointerMustBeInInheritanceChainOfActuallyDeserializedObjectType1) +{ + std::shared_ptr<Base> baseData1{ new MoreDerived{ 3, 7, 10 } }; + std::shared_ptr<Base> baseData2{}; + baseData2 = baseData1; + auto& ser = createSerializer(); + ser.ext(baseData1, StdSmartPtr{}); + ser.ext(baseData2, StdSmartPtr{}); + + std::shared_ptr<Base> baseRes1{}; + std::shared_ptr<Derived> baseRes2{}; + auto& des = createDeserializer(); + des.ext(baseRes1, StdSmartPtr{}); + des.ext(baseRes2, StdSmartPtr{}); + + EXPECT_THAT(baseRes2.get(), ::testing::NotNull()); + auto err = des.adapter().error(); + EXPECT_THAT(err, Eq(bitsery::ReaderError::NoError)); + EXPECT_TRUE(isPointerContextValid()); +} + +TEST_F( + SerializeExtensionStdSmartSharedPtr, + PolymorphicObservedPointerMustBeInInheritanceChainOfActuallyDeserializedObjectType2) +{ + std::shared_ptr<Base> baseData1{ new MoreDerived{ 3, 7, 10 } }; + std::shared_ptr<Base> baseData2{}; + baseData2 = baseData1; + auto& ser = createSerializer(); + ser.ext(baseData1, StdSmartPtr{}); + ser.ext(baseData2, StdSmartPtr{}); + + std::shared_ptr<Base> baseRes1{}; + std::shared_ptr<DerivedSibling> baseRes2{}; + auto& des = createDeserializer(); + des.ext(baseRes1, StdSmartPtr{}); + des.ext(baseRes2, StdSmartPtr{}); + + EXPECT_THAT(baseRes2.get(), ::testing::IsNull()); + auto err = des.adapter().error(); + EXPECT_THAT(err, Eq(bitsery::ReaderError::InvalidPointer)); + EXPECT_TRUE(isPointerContextValid()); +} + struct TestSharedFromThis : public std::enable_shared_from_this<TestSharedFromThis> {
Vulnerability mechanics
Root cause
"Missing type-compatibility validation in `loadFromSharedState` allows a shared pointer to be assigned to an object of a different, incompatible type during deserialization."
Attack vector
An attacker sends a crafted serialized payload that causes a shared pointer to be deserialized as a different type than the one it was originally serialized as. During deserialization, the library previously trusted the input to determine whether a shared pointer referred to an already-deserialized object; if so, it would assign the pointer without verifying that the observed type is actually compatible with the statically declared type [ref_id=1]. This allows the attacker to make a `shared_ptr<Base>` point to memory laid out as a completely unrelated type (e.g. `std::vector`), potentially leaking internal object addresses and bypassing ASLR [ref_id=1]. The attack is remote, requires no authentication, and has low complexity (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L).
Affected code
The vulnerability resides in `include/bitsery/ext/std_smart_ptr.h` in the `loadFromSharedState` function, and in the deserialization logic within `include/bitsery/ext/utils/pointer_utils.h` (the `PointerObjectExtensionBase` class). The patch also modifies `include/bitsery/ext/utils/polymorphism_utils.h` to add a `_derivedToBaseArray` map and a `canAssignToShared` check.
What the fix does
The patch adds a `_derivedToBaseArray` map to `PolymorphicContext` that records which base types each derived type directly inherits from. During deserialization of shared polymorphic pointers, the new `canAssignToShared` method walks this hierarchy chain to verify that the actual deserialized type is in the inheritance chain of the statically declared pointer type [patch_id=2539783]. If the types are incompatible, the deserializer sets a `ReaderError::InvalidPointer` error instead of blindly assigning the pointer. For non-polymorphic shared pointers, a similar direct type-ID equality check is added. The patch also introduces `AbstractPolymorphicHandler` to properly register abstract base classes in the hierarchy, and fixes pointer-address tracking for virtual inheritance via `getRootPtr`.
Preconditions
- configThe application must use bitsery's StdSmartPtr extension to deserialize shared pointers from untrusted data.
- networkThe attacker must be able to supply a crafted serialized payload over the network.
- authNo authentication or prior access is required.
Reproduction
The test file `tests/serialization_ext_std_smart_ptr.cpp` (added in the patch) contains three reproduction test cases. The first test, `NonPolymorphicObservedPointerMustMatchActuallyDeserializedObjectType`, serializes a `shared_ptr<vector<uint8_t>>` and a `shared_ptr<MightMatchVecLayout>`, then hacks the buffer so that during deserialization the second pointer references the first object's ID. Without the fix, the deserializer would blindly assign the `MightMatchVecLayout` pointer to the `vector`'s memory, leaking its internal representation. The second and third tests (`PolymorphicObservedPointerMustBeInInheritanceChain...`) verify that a polymorphic shared pointer deserialized through a base type can only be assigned to a derived type that is actually in its inheritance chain, not to an unrelated sibling class.
Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- gist.github.com/TrebledJ/750abc64a826f19dd2d6774724629b71nvd
- github.com/fraillt/bitsery/blob/master/CHANGELOG.mdnvd
- github.com/fraillt/bitsery/commit/66d16516e24893bebc1c8af52bf2fe9ad0735061nvd
- github.com/fraillt/bitsery/releases/tag/v5.2.5nvd
- vuldb.com/submit/814457nvd
- vuldb.com/vuln/365541nvd
- vuldb.com/vuln/365541/ctinvd
News mentions
0No linked articles in our index yet.