Case 74: Internal detail:: base class layout change leaks via public API¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
internal_type_leaks_via_public_api |
| Source files | examples/case74_detail_base_class_changed/ |
Category: Internal-leak | Verdict: BREAKING
What breaks¶
The library declares its "internal" implementation helpers inside a
detail:: namespace โ matching the convention used by oneDAL, oneTBB,
many Boost libraries, and the standard library's std::__detail:
namespace mylib::detail {
class descriptor_base { /* ... */ };
}
class knn_descriptor : public detail::descriptor_base { /* ... */ };
v2 adds an int max_iter_ member to detail::descriptor_base. From
the library author's perspective this is purely an internal change.
From consumers' perspective it's a binary ABI break:
sizeof(knn_descriptor)increases by 4โ8 bytes (alignment-dependent).- The offset of
neighbor_count_(declared in the derived public class) shifts because the base subobject grew. - Stack-allocated
knn_descriptorinstances in caller code overflow their pre-v2 frame slots. - Heap-allocated objects from callers compiled against v1 headers under-allocate when run against v2 binaries.
Real Failure Demo¶
Severity: BREAKING / MEMORY CORRUPTION
cmake -S examples -B /tmp/abicheck-examples-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/abicheck-examples-build --target case75_detail_base_class_changed_app case75_detail_base_class_changed_v2
tmp=$(mktemp -d)
cp /tmp/abicheck-examples-build/case75_detail_base_class_changed/app_v1 "$tmp/"
cp /tmp/abicheck-examples-build/case75_detail_base_class_changed/libv2.so "$tmp/libv1.so"
(cd "$tmp" && LD_LIBRARY_PATH=. ./app_v1)
# *** stack smashing detected ***: terminated
Why abicheck catches it¶
Existing detectors already report type_size_changed on
detail::descriptor_base. By itself that finding is easy for a
reviewer to dismiss as "internal-only". The
internal_type_leaks_via_public_api overlay added in this PR walks
the reachability graph from every public exported symbol and surfaces
a synthetic finding that names the public-facing class
(mylib::knn_descriptor) together with the chain
knn_descriptor โ base:detail::descriptor_base, making the public
impact impossible to miss.
Code diff¶
// v1
namespace mylib::detail {
class descriptor_base {
public:
int class_count_;
};
}
// v2 โ single new field in the "internal" base
namespace mylib::detail {
class descriptor_base {
public:
int class_count_;
int max_iter_; // NEW โ shifts every derived layout
};
}
How to fix¶
Treat detail:: as a private implementation surface that is not
allowed to leak through the binary interface. The two main patterns
are pimpl (hold a pointer to an opaque struct) and a true private
interface (forward-declared header consumers cannot include):
// Pimpl โ internal layout changes never affect the public sizeof.
class knn_descriptor {
public:
knn_descriptor();
~knn_descriptor();
int get_class_count() const;
private:
struct impl;
impl* p_; // size is fixed at sizeof(void*)
};
References¶
- Itanium C++ ABI ยง2.5: object layout and base subobjects.
- oneDAL coding guidelines:
detail::symbols are explicitly documented as unstable, but the layout still leaks through public derived classes if pimpl is not used.
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.