Files
strangecpp/namecollision/README.md
2026-02-23 16:29:49 +01:00

150 lines
5.1 KiB
Markdown

# Name Collision / ODR Violation with Shared Libraries
## The Setup
`hello.cpp` defines a class `nt` with a method `print()`, used internally by
`print_obj()`. `main.cpp` defines a `namespace nt` with a free function
`print()`, and calls both `nt::print()` and `print_obj()`.
`hello.cpp` is compiled into a shared library (`libhello.so` on Linux,
`libhello.dll` on Windows), and `main.cpp` is linked against it.
## The Problem
C++ name mangling encodes both **namespaces** and **classes** identically in the
Itanium ABI (used on Linux):
```
namespace nt { void print() } → _ZN2nt5printEv
class nt { void print() } → _ZN2nt5printEv
```
Both produce the exact same mangled symbol. The compiler has no way to
distinguish them at link time.
## What Actually Happens
With `-O0` (no optimization):
- `libhello.so` exports `_ZN2nt5printEv` as a **weak** symbol (the class method)
- `main` defines `_ZN2nt5printEv` as a **strong/global** symbol (the namespace
function)
- When `print_obj()` in the shared library calls `nt::print()`, the dynamic
linker resolves `_ZN2nt5printEv` at runtime and the **strong symbol in `main`
wins**
- Result: `print_obj()` calls `main`'s `nt::print()` instead of the class method
- Output: `"Hello from namespace"` printed twice
With `-O3` (optimizations on):
- The compiler inlines the class `nt::print()` directly into `print_obj()`
- `_ZN2nt5printEv` is never emitted as a standalone symbol in `libhello.so`
- No collision occurs — each call resolves correctly
- Output: correct
This is why the bug was **optimization-dependent** and hard to spot.
## Symbol Evidence
```
# -O0: libhello.so exports the class method as weak
00000000000011d0 w F .text _ZN2nt5printEv ← weak, can be overridden
# -O3: libhello.so does NOT export it at all (inlined away)
# only _Z9print_objv is present
```
## One Definition Rule (ODR)
The C++ standard (basic.def.odr) requires that any entity with more than one
definition across translation units must have **identical** definitions.
Violating this is **ill-formed, no diagnostic required** — the compiler and
linker are not required to warn you.
The consequences are undefined behavior: the program may crash, produce wrong
output, or appear to work correctly depending on compiler flags, optimization
level, or link order.
## Linux Dynamic Linker Behavior
On Linux, the dynamic linker (`ld.so`) uses a **flat symbol namespace** by
default. When a shared library references a symbol, the linker searches all
loaded objects (including the main executable) and resolves to the **first
strong symbol found**, regardless of which DSO the symbol logically belongs to.
This is different from macOS (which uses two-level namespaces by default) and
Windows (where DLL symbols are explicitly imported/exported via import tables
and don't collide this way).
This flat namespace behavior means a weak symbol in a `.so` can be silently
overridden by any strong symbol of the same name anywhere in the process — even
from the main executable.
## Why Windows Is Not Affected
On Windows, this collision does not occur. The DLL symbol model is fundamentally
different:
- Only symbols explicitly marked `__declspec(dllexport)` are visible outside the
DLL — everything else is private by default
- The linker generates an import library (`.lib`) with stubs that reference the
DLL by name, so the loader knows exactly which DLL each symbol comes from
- There is no flat global symbol namespace — each DLL is its own isolated scope
This means the internal `class nt` in `hello.cpp` was never at risk on Windows
regardless of optimization level. The bug is Linux (and ELF) specific.
To keep `hello.cpp` portable, the `__declspec(dllexport)` on `print_obj` is
wrapped in a macro:
```cpp
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
```
On Linux `EXPORT` expands to nothing. On Windows it marks the symbol for export.
## The Linux Fix
Wrap internal-use classes in an **anonymous namespace** in `hello.cpp`:
```cpp
namespace {
class nt {
public:
void print() { ... }
};
}
```
This gives the class **internal linkage**. The mangled symbol becomes:
```
_ZN12_GLOBAL__N_12nt5printEv
```
The `_GLOBAL__N_1` prefix is the ABI encoding for the anonymous namespace,
making it unique per translation unit and invisible to the dynamic linker. No
collision is possible.
The Linux and Windows fixes solve the same problem from opposite defaults:
- Linux: **opt-in to hiding** symbols (anonymous namespace, `-fvisibility=hidden`)
- Windows: **opt-in to exporting** symbols (`__declspec(dllexport)`)
## Key Takeaways
- Class and namespace names mangle identically in the Itanium C++ ABI
- Weak symbols in shared libraries can be silently hijacked by strong symbols in
the executable
- ODR violations are UB with no required diagnostic — bugs may only appear at
certain optimization levels
- This is a Linux/ELF-specific problem — Windows DLLs use explicit export tables
and are not affected
- Internal implementation details in shared libraries should use anonymous
namespaces to prevent symbol leakage on Linux
- Use `objdump -t` or `nm` to inspect symbol visibility and catch these issues
early