#1105·oatpp

Type confusion in 'Tree::deleteValueObject'

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

Summary

mapping::Tree keeps every variant's payload in a single v_uint64 slot and selects the reinterpret_cast from a separate type tag. All of its setters commit the tag first, then perform an allocation that can throw, then store the payload. If the allocation throws, the node is left claiming the new type while the slot still holds the previous variant's raw 64 bits. The destructor then picks its delete from the tag and deletes that integer as a pointer.

Details

Storage, src/oatpp/data/mapping/Tree.hpp:108,113:

108      typedef v_uint64 LARGEST_TYPE;
...
113      LARGEST_TYPE m_data;

Tree::setVector (src/oatpp/data/mapping/Tree.cpp:418-423) shows the ordering that every setter uses:

418    void Tree::setVector(v_uint64 size) {
419      deleteValueObject();
420      m_type = Type::VECTOR;
421      auto data = new std::vector<Tree>(size);
422      m_data = reinterpret_cast<LARGEST_TYPE>(data);
423    }

Line 420 commits the tag. Line 421 can throw std::length_error for an oversized size, or std::bad_alloc. Line 422 never runs, and the node survives with m_type == VECTOR over the previous variant's bytes. setString, setMap and setPairs (:389-449) have the same shape, and setCopy (:276-350) throws from its null guards at :319, :327 and :337 after the tag is already set.

Every consumer keys off the tag, including the destructor (src/oatpp/data/mapping/Tree.cpp:224-243):

229      case Type::VECTOR: {
230        auto data = reinterpret_cast<std::vector<Tree> *>(m_data);
231        delete data;
232        break;
233      }

Fix: store the payload before committing the tag, so a throw leaves the node in its previous consistent state.

       auto data = new std::vector<Tree>(size);
       deleteValueObject();
       m_data = reinterpret_cast<LARGEST_TYPE>(data);
       m_type = Type::VECTOR;

PoC

poc_Tree_deleteValueObject.cpp.:

cpp
Tree t;
t.setPrimitive<v_uint64>(0x4142434445464748ULL);   // an ordinary integer
t.setVector(0xFFFFFFFFFFFFFFFFULL);                // tag -> VECTOR, then length_error
// ~Tree -> deleteValueObject() -> delete (std::vector<Tree>*)0x4142434445464748

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_Tree_deleteValueObject.cpp liboatpp.a -lpthread -o poc_Tree_deleteValueObject
ASAN_OPTIONS=halt_on_error=0:new_delete_type_mismatch=1 ./poc_Tree_deleteValueObject

ASAN output :

[*] before: type=12 (UINT_64=12)
[*] caught: cannot create std::vector larger than max_size()
[!] after : type=16 (VECTOR=16)  <-- tag moved, payload did not
[!] isVector()=1  -> every getVector() now returns *(vector<Tree>*)0x4142434445464748
==...==ERROR: AddressSanitizer: SEGV
    [...]
    #1 in oatpp::data::mapping::Tree::deleteValueObject()  src/oatpp/data/mapping/Tree.cpp:231:7
    #2 in oatpp::data::mapping::Tree::~Tree()              src/oatpp/data/mapping/Tree.cpp:180:3

Impact

delete applied to a caller-supplied 64-bit value as a typed pointer, plus the matching read variant through getVector() and friends. and this is API level issue in a public class rather than a remote one.