## Appendix: Reproduction details ### A1. Setup and reproduction Build environment (Dockerfile): ```dockerfile # GCC 10 is used because the bundled library's __cxa_pure_virtual # binding (U, not w) matches GCC 10 but not GCC 11+. The libstdc++ # imports (std::ios_base::Init::Init/~Init) are consistent with any # GCC version prior to 13. # # CMake 2.8.12.2 also requires GCC 10 or earlier to compile # (it fails with GCC 11+). FROM ubuntu:24.04 RUN apt-get update -qq \ && apt-get install -qq -y gcc-10 g++-10 zlib1g-dev binutils make wget > /dev/null RUN cd /tmp \ && wget -q https://cmake.org/files/v2.8/cmake-2.8.12.2.tar.gz \ && tar xzf cmake-2.8.12.2.tar.gz && cd cmake-2.8.12.2 \ && CC=gcc-10 CXX=g++-10 ./bootstrap --prefix=/opt/cmake > /dev/null 2>&1 \ && make -j$(nproc) > /dev/null 2>&1 \ && make install > /dev/null 2>&1 ENV PATH="/opt/cmake/bin:$PATH" RUN cd /tmp \ && wget -q https://taglib.org/releases/taglib-1.9.1.tar.gz \ && tar xzf taglib-1.9.1.tar.gz ``` Build and run: ``` docker build -t taglib-env . docker run --rm -it taglib-env ``` Inside the container, copy the source tree and apply the one-line patch to the copy: ```sh cd /tmp cp -a taglib-1.9.1 taglib-1.9.1-patched sed -i 's/~ListPrivate() {/virtual ~ListPrivate() {/' \ taglib-1.9.1-patched/taglib/toolkit/tlist.tcc ``` Build both with the same flags. The flag `-DStringPrivate=MyPrivateString` reproduces the class rename; whether the actual build uses this flag or a source patch is unknown, but the effect on the dynamic symbol table is the same. The bundled library's dynamic symbol table includes `vsnprintf@GLIBC_2.2.5`, which is called only from `debug()` and `debugData()` in tdebug.cpp. This indicates the library was built without `-DNDEBUG`. ```sh FLAGS="-O3 -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 \ -std=c++11 -DStringPrivate=MyPrivateString \ -fno-stack-protector -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0" # (A) Unmodified mkdir build-unmod && cd build-unmod cmake /tmp/taglib-1.9.1 -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 -DCMAKE_CXX_FLAGS="$FLAGS" make -j$(nproc) cd /tmp # (B) Patched mkdir build-patched && cd build-patched cmake /tmp/taglib-1.9.1-patched -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 -DCMAKE_CXX_FLAGS="$FLAGS" make -j$(nproc) cd /tmp ``` Compare dynamic symbols (within the same build, addresses are comparable): ```sh nm -D build-unmod/taglib/libtag.so.1 | c++filt | sort > sym-unmod.txt nm -D build-patched/taglib/libtag.so.1 | c++filt | sort > sym-patched.txt diff sym-unmod.txt sym-patched.txt ``` To compare against the bundled library, extract `libtag.so.1` from the Ardour 9.0 x86_64 Linux installer (`Ardour-9.0.0-x86_64.run`, downloaded 2026-02-15, SHA256: `713f11881a502585fffe3eade58ed41e0fe290983eae794f328012876767b4d1`): ```sh ./Ardour-9.0.0-x86_64.run --noexec --target ardour-extracted nm -D ardour-extracted/Ardour_x86_64-9.0.0/lib/libtag.so.1 | c++filt | sort > sym-bundled.txt ``` Cross-build comparison requires stripping addresses (different builds produce different layouts): ```sh sed 's/^[0-9a-f]* //' sym-patched.txt | sort > sym-patched-names.txt sed 's/^[0-9a-f]* //' sym-bundled.txt | sort > sym-bundled-names.txt diff sym-patched-names.txt sym-bundled-names.txt ``` ### A2. Results (A) Unmodified (B) Patched Ardour bundled Total symbols 2819 2872 2872 ListPrivate vtables 0 8 8 ListPrivate typeinfo 0 9 9 ListPrivateBase typeinfo 0 2 2 RefCounterOld typeinfo 0 2 2 MyPrivateString vtable/ti 2 2 2 Build (A) is missing 53 symbols present in the bundled library (2872 - 2819). Build (B), with only the one-line patch, produces a dynamic symbol table identical to the bundled library's: all 2872 symbols match in name and binding type. ### A3. Explanation The StringPrivate rename produces a vtable for MyPrivateString because String::StringPrivate inherits from RefCounter, which has a virtual destructor. This is expected and unrelated to the `ListPrivate` patch. However, the vtables for ListPrivate cannot be produced by any combination of compiler flags. The inheritance chain is: RefCounterOld (no virtual functions, no destructor declared) └─ ListPrivateBase (no virtual functions) └─ ListPrivate (destructor is non-virtual in upstream) Per the C++ object model, no vtable or polymorphic RTTI is emitted without at least one virtual function in the hierarchy, regardless of optimization level, visibility settings, or C++ standard version. Adding `virtual` to `~ListPrivate()` in the pointer-type partial specialization makes that class polymorphic. This produces exactly the 8 vtable entries, 9 typeinfo entries, and 2 `ListPrivateBase` + 2 `RefCounterOld` base-class typeinfo entries seen in the bundled library, matching the total of 2872 symbols. ### A4. Cross-check against taglib Git history I also checked whether any commit in the taglib repository could produce vtables for `ListPrivate` while `RefCounterOld` is part of the inheritance chain. Per the C++ object model, this requires at least one virtual function somewhere in the `RefCounterOld -> ListPrivateBase -> ListPrivate` hierarchy (not necessarily in `ListPrivate` itself). I scanned all 3,015 commits at https://github.com/taglib/taglib (as of 2026-02-17). Of those, 1,112 commits contain `RefCounterOld` in `trefcounter.h`, spanning 12 unique file-state combinations. Each was checked for `virtual` or `override` in any of the three classes: RefCounterOld (5 unique versions): No virtual functions or virtual destructors in any version. ListPrivateBase (6 unique versions, when inheriting from RefCounterOld): No virtual functions, no virtual or override keywords. ListPrivate (6 unique versions, same commits): No virtual functions, no virtual or override keywords. Additionally, there is no commit where `RefCounterOld` exists and `ListPrivateBase` inherits from `RefCounter` (which does have a virtual destructor). When `RefCounterOld` is present, `ListPrivateBase` always inherits from `RefCounterOld`. For completeness: between commits 9867bc94 and a1bdb017 (July 2023), `~ListPrivate()` was marked `override`, but in those commits `ListPrivateBase` inherited from `RefCounter` (not `RefCounterOld`), so `RefCounterOld` was not in the inheritance chain and its typeinfo would not be emitted. No commit in the repository produces a polymorphic `ListPrivate` hierarchy rooted in `RefCounterOld`. The bundled binary's symbol table cannot be reproduced from any upstream commit or tagged release.