Type confusion in 'Tree::deleteValueObject'
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.:
Tree t;
t.setPrimitive<v_uint64>(0x4142434445464748ULL); // an ordinary integer
t.setVector(0xFFFFFFFFFFFFFFFFULL); // tag -> VECTOR, then length_error
// ~Tree -> deleteValueObject() -> delete (std::vector<Tree>*)0x4142434445464748Build 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_Tree_deleteValueObject.cpp liboatpp.a -lpthread -o poc_Tree_deleteValueObject
ASAN_OPTIONS=halt_on_error=0:new_delete_type_mismatch=1 ./poc_Tree_deleteValueObjectASAN 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:3Impact
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.
Source: oatpp/oatpp