#1101·oatpp

SEGV in 'ObjectToTreeMapper::mapTree'

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

Summary

mapTree static_casts the value pointer to mapping::Tree* and copy-assigns from it. The only thing establishing that the pointee is a Tree is the type tag that selected mapTree in the first place. Tree's copy assignment is not a shallow copy, it recurses into Attributes, which builds an unordered_map and follows pointers out of the source object, so a non Tree source dereferences garbage.

Details

src/oatpp/data/mapping/ObjectToTreeMapper.cpp:105-112:

105    void ObjectToTreeMapper::mapTree(const ObjectToTreeMapper* mapper, State& state, const oatpp::Void& polymorph) {
106      if(!polymorph) {
107        state.tree->setNull();
108        return;
109      }
110      auto tree = static_cast<mapping::Tree*>(polymorph.get());
111      *state.tree = *tree; // copy
112    }

polymorph.get() is a void*. This function is registered in the dispatch table under Tree's ClassId, and ObjectToTreeMapper::map picks it by tag, so line 110 is reached whenever the tag says Tree — regardless of what the pointer owns.

Tree::operator= copies m_attributes as well as the payload, and Attributes::operator= constructs an Attrs containing an unordered_map, reading the source's attribute storage. Against a std::string source that storage is whatever the string's bytes happen to be.

json/ObjectMapper.cpp:50 and ObjectRemapper.cpp:49 perform the same static_cast<const Tree*> guarded by getValueType() == Tree::Class::getType(). That guard is the same tag comparison, so it holds only as far as the tag is trustworthy.

Fix: the tag has to be made trustworthy rather than checked again here.

PoC

poc_ObjectToTreeMapper_mapTree.cpp:

cpp
auto handle = std::make_shared<oatpp::data::type::AnyHandle>(
    std::make_shared<std::string>("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
    oatpp::Tree::Class::getType());          // tag says Tree, payload is std::string

oatpp::Any any(handle, nullptr);

oatpp::json::ObjectMapper mapper;
auto out = mapper.writeToString(any);        // mapAny -> map -> mapTree -> *state.tree = *tree

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

ASAN output :\

[*] sizeof(mapping::Tree)=32 sizeof(std::string)=32

==...==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000028
    [...]
    #2 in oatpp::data::mapping::Tree::Attributes::Attrs::Attrs(...)          src/oatpp/data/mapping/Tree.cpp
    #3 in oatpp::data::mapping::Tree::Attributes::operator=(Attributes const&) src/oatpp/data/mapping/Tree.cpp

Impact

wild pointer dereference during a copy, driven by whatever the misinterpreted source object contains.

Reachability is application level

  • an AnyHandle whose pointer and type disagree, which mapAny will then promote to a first-class tag.