Skip to content

fix(executorch): share the C++ runtime instead of shipping a second copy - #4592

Open
shoumikhin wants to merge 1 commit into
pytorch:mainfrom
shoumikhin:fix/et-runtime-shared-cxx-runtime
Open

fix(executorch): share the C++ runtime instead of shipping a second copy#4592
shoumikhin wants to merge 1 commit into
pytorch:mainfrom
shoumikhin:fix/et-runtime-shared-cxx-runtime

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Depends on #4581, which is included here so CI can reach the crash. executorch-runtime-test is gated on executorch-runtime-build, and that build fails on main today, so the test is skipped rather than red and this crash never shows up. Rebase drops the first two commits once #4581 lands.

Description

The prebuilt ExecuTorch runtime wheel statically links libstdc++ into every shared object it ships. Loading it segfaults.

_portable_lib.so lists libaoti_cuda_shims.so as its first DT_NEEDED, and no object in the wheel declares libstdc++.so.6, so the shim leads the dlopen group in symbol search order and everything loaded alongside it resolves the C++ runtime against the wheel. Measured with LD_DEBUG=bindings on a built wheel, libnvinfer.so.11 resolves 110 symbols into libaoti_cuda_shims.so and 22 into _portable_lib.so, against 6 into libstdc++.so.6. Among them are the __cxxabiv1 type_info vtables and the std::locale internals, which have to be unique in a process.

Two libstdc++ builds then share one process. A locale facet built by one gets indexed with the other's std::locale::id, so a virtual call lands on the wrong slot and stores through a garbage pointer. The crash lands in the shim's own std::codecvt<char16_t, char, mbstate_t>::do_unshift, four bytes in, on a store through the fifth argument register.

This is why executorch-runtime-test exits 139 while probing the extension. The job is gated on executorch-runtime-build, which has been failing, so it has been skipped rather than red and the crash was never visible.

The fix

Link the C++ runtime dynamically. libtorch_cpu.so, libc10.so, libtorch_python.so and libnvinfer.so.11 all already carry DT_NEEDED libstdc++.so.6 and need no more than GLIBCXX_3.4.22, so a shared libstdc++ is already present in any process that can load this wheel, and the second copy protects against nothing.

Hiding the static copy is not an alternative. Private __cxxabiv1 type_info stops catch (std::exception&) from matching across a library boundary and makes dynamic_cast return null, which trades a crash for silent wrong answers. A private C++ runtime is only sound in a shared object whose whole interface is C, and _portable_lib.so hands at::Tensor and std::string across.

The placement analysis in the CMake file stays as it was. The toolchain hands CMake the C driver, which links no C++ runtime, and its injected -lstdc++ sits ahead of the object files wrapped in --as-needed, where it resolves nothing. That fragment still has to be stripped. Only what replaces it changes, from libstdc++.a after the objects to -lstdc++ after the objects.

The build check is inverted to match. It now fails when a shipped object defines libstdc++'s own symbols, and when an object references the C++ runtime without declaring where it comes from, which is the missing exception_ptr::_M_addref the old check was written for.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code follows the style guidelines of this project (You can use the linters)
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas and hacks
  • I have made corresponding changes to the documentation
  • I have added tests to verify my fix or my feature
  • New and existing unit tests pass locally with my changes
  • I have added the relevant labels to my PR in so that relevant reviewers are notified

@meta-cla meta-cla Bot added the cla signed label Aug 26, 2026
@shoumikhin
shoumikhin force-pushed the fix/et-runtime-shared-cxx-runtime branch from 656883f to 2659c08 Compare August 26, 2026 15:31
@lanluo-nvidia lanluo-nvidia added this to the v2.14.0 milestone Aug 26, 2026
The prebuilt ExecuTorch runtime wheel statically links libstdc++ into every
shared object it ships, and loading it segfaults.

_portable_lib.so lists libaoti_cuda_shims.so as its first DT_NEEDED, and no
object in the wheel declares libstdc++.so.6, so the shim leads the dlopen group
in symbol search order and everything loaded alongside it resolves the C++
runtime against the wheel. Measured with LD_DEBUG=bindings on a built wheel,
libnvinfer.so.11 resolves 110 symbols into libaoti_cuda_shims.so and 22 into
_portable_lib.so, against 6 into libstdc++.so.6. Among them are the __cxxabiv1
type_info vtables and the std::locale internals, which have to be unique in a
process.

Two libstdc++ builds then share one process. A locale facet built by one gets
indexed with the other's std::locale::id, so a virtual call lands on the wrong
slot and stores through a garbage pointer.

Link the C++ runtime dynamically instead. libtorch_cpu.so, libc10.so,
libtorch_python.so and libnvinfer.so.11 all already carry DT_NEEDED
libstdc++.so.6 and need no more than GLIBCXX_3.4.22, so a shared libstdc++ is
already in any process that can load this wheel, and the second copy protects
against nothing. Hiding the static copy is not an alternative: private
__cxxabiv1 type_info stops catch (std::exception&) from matching across a
library boundary and makes dynamic_cast return null, which trades a crash for
silent wrong answers.

The placement analysis stays as it was. The toolchain hands CMake the C driver,
which links no C++ runtime, and its injected -lstdc++ sits ahead of the objects
wrapped in --as-needed, where it resolves nothing. That fragment still has to be
stripped. Only what replaces it changes, from libstdc++.a after the objects to
-lstdc++ after the objects.

The build check is inverted to match. It now fails when a shipped object defines
libstdc++'s own symbols, and when an object references the C++ runtime without
declaring where it comes from, which is the missing exception_ptr::_M_addref the
old check was written for.
@shoumikhin
shoumikhin force-pushed the fix/et-runtime-shared-cxx-runtime branch from 2659c08 to 8c2bf73 Compare August 26, 2026 18:00
@github-actions github-actions Bot added the component: api [Python] Issues re: Python API label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants