Type confusion in 'Environment::getComponent'
Summary
The dependency-injection registry stores components as void* keyed by typeid(T).name(), and OATPP_COMPONENT reinterpret_casts the result back to the requested type. String equality is the entire type check. On the Itanium ABI every unnamed namespace mangles to _GLOBAL__N_1, so two structurally different types declared in the unnamed namespaces of two translation units produce the same key. Whichever registers first is handed to whoever asks, cast to the asker's type.
Details
src/oatpp/macro/component.hpp:43-57:
43 #define OATPP_MACRO_GET_COMPONENT_1(TYPE) \
44 (*(reinterpret_cast<TYPE*>(oatpp::Environment::getComponent(typeid(TYPE).name()))))
...
53 #define OATPP_MACRO_COMPONENT_1(TYPE, NAME) \
54 TYPE& NAME = (*(reinterpret_cast<TYPE*>(oatpp::Environment::getComponent(typeid(TYPE).name()))))Environment::getComponent (src/oatpp/Environment.cpp:364 and the qualified overload at :378) looks the key up and returns a void*. The registry stores void* (Environment.hpp:301, :313-354). No type identity accompanies the value, so the mangled name string is the only thing standing between a component and a wrong cast.
typeid(T).name() is not injective across translation units. Itanium mangles an unnamed namespace as _GLOBAL__N_1 with no per-TU discriminator in the type_info name, so namespace { struct Handler; } in two different .cpp files yields the identical string. The two types can have different sizes and layouts.
Registration is not always silent about this. If both translation units register, registerComponent (Environment.cpp:336-344) throws on the duplicate name and the failure is loud. The dangerous arrangement is asymmetric: one TU registers, the other only fetches.
A qualifier mismatch throws rather than falling back to another component (Environment.cpp:378-390), and an ambiguous lookup with more than one candidate throws as well.
Fix: store a std::type_index alongside the void* and compare it on retrieval.
type_info::operator== distinguishes unnamed-namespace types correctly; the mangled name string does not.
PoC
poc_Environment_getComponent.cpp: The offending reinterpret_cast is the body of OATPP_MACRO_COMPONENT_1 (component.hpp:53-54), expanded at the call site, which is why frame #0 lands in the PoC.
The file is compiled twice into two translation units, which is what a cross-TU typeid collision requires:
#if defined(POC_TU_A)
namespace { struct Handler { char pad[16]; }; } // 16 bytes, TU-local
void tuA_register() {
static Handler h{};
std::memset(h.pad, 'A', sizeof(h.pad));
new oatpp::Environment::Component<Handler>("NoName", h); // leaked on purpose: must stay registered
}
#elif defined(POC_TU_B)
namespace { struct Handler { long a, b, c, d, e, f, g, h; }; } // 64 bytes, same name
int main() {
oatpp::Environment::init();
tuA_register(); // A registers its 16-byte object
OATPP_COMPONENT(Handler, got); // B asks for its 64-byte one and gets A's
std::printf("B read a=%ld ... h=%ld\n", got.a, got.h);
}
#endifBuild and run:
FL="-fsanitize=address,undefined,vptr -fsanitize-recover=all -fno-omit-frame-pointer -frtti -g -O1 -std=c++17"
clang++-22 $FL -DPOC_TU_A -I<oatpp>/src -c poc_Environment_getComponent.cpp -o a.o
clang++-22 $FL -DPOC_TU_B -I<oatpp>/src -c poc_Environment_getComponent.cpp -o b.o
clang++-22 $FL a.o b.o liboatpp.a -lpthread -o poc_Environment_getComponent
ASAN_OPTIONS=halt_on_error=0 ./poc_Environment_getComponentASAN output:
TU A typeid name : N12_GLOBAL__N_17HandlerE
TU B typeid name : N12_GLOBAL__N_17HandlerE
keys are equal : 1 <-- the whole type check
sizeof A::Handler=16 sizeof B::Handler=64
==...==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c04557e0108 ...
READ of size 8
0x7c04557e0108 is located 40 bytes after 80-byte regionThe 80-byte region is the Component<Handler> object holding TU A's 16-byte value; TU B reads it as 64 bytes and runs off the end.
Impact
OOB read or write of the difference between the two types' sizes, depending on what the fetching TU does with the reference. OATPP_COMPONENT binds a TYPE&, so the caller can write through it as easily as read. Reachability is application level and needs a specific arrangement: two translation units with same-named unnamed-namespace types, one registering and the other only fetching.
Source: oatpp/oatpp