Fix ChArchiveOut serializing a dangling temporary for std::map and st… - #774
Open
DanNegrut wants to merge 1 commit into
Open
Fix ChArchiveOut serializing a dangling temporary for std::map and st…#774DanNegrut wants to merge 1 commit into
DanNegrut wants to merge 1 commit into
Conversation
…d::unordered_map Dereferencing a std::map<T, Tv> iterator yields std::pair<const T, Tv>&, but the out() wrappers for std::map and std::unordered_map declared their ChNameValue with a non-const key. The two types differ, so the compiler materialized a temporary pair, and ChNameValue stores only a raw pointer to its value (it takes const T& and keeps (T*)&mvalue). Two things followed: the archive wrote a copy of the element rather than the element itself, and that copy had already been destroyed by the time it was written, so the write read freed memory. The visible effect depended on the value type. A trivially destructible type such as ChVector3d usually still read back its old bytes and looked correct, which is why this survived so long. A polymorphic type with a non-trivial destructor lost everything below its base class: serializing a std::map<std::string, ChLinkTSDA> produced a 254 byte record holding only the ChObj fields, with no _version_ChLinkTSDA, no spring coefficients and no bodies. Reported in #507, where it originally presented as a segmentation fault. The fix declares the key const in the two out() wrappers, so the ChNameValue binds directly to the real map element and no temporary is created. The in() path is deliberately left alone: _wrap_pair::ArchiveIn writes into first, so a const key there would assign through a pointer to a const object. Because member functions of a class template are instantiated only when used, ArchiveIn is never instantiated on the const-keyed specialization, and only ArchiveOut, which merely reads first, is affected. The naive alternative, giving the temporary a name so that it outlives the call, would compile and stop the crash but would keep serializing a copy. That is not equivalent: ChLink's copy constructor nulls both body pointers, so it would trade a loud bug for a silent one. Adds five regression tests to utest_CH_archive. They assert that the archive does not copy the container element, which is deterministic, rather than asserting on how the resulting undefined behavior happens to manifest, which varies with type and optimizer. vector and list are included as controls. With the two-line change reverted, the map and unordered_map tests fail (one copy observed, and the value reads back as the test payload's destructor poison) while the vector and list controls still pass. This file previously had no STL container coverage at all, and the only map in demo_CH_archive is an unordered_map<int, double> of PODs with no assertions, which is why nothing in tree caught this. Unrelated and pre-existing, noted here only because this fix makes it visible in serialized output: ChObj's copy constructor never assigns m_tag, so any copied or cloned Chrono object carries a garbage tag. Confirmed without any serialization involved, where a ChBody tagged 77 reports 32765 after a copy. That is left for a separate report. Verified with clang (ROCm 7.1) on Windows, Release: all 10 core unit test binaries pass (44 cases) and demo_CH_archive still round-trips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
|
@DanNegrut - I assigned Dario Mangoni as a reviewer. The archiving stuff was implemented at Parma and I am not very familiar with it. |
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #776
…d::unordered_map
Dereferencing a std::map<T, Tv> iterator yields std::pair<const T, Tv>&, but the out() wrappers for std::map and std::unordered_map declared their ChNameValue with a non-const key. The two types differ, so the compiler materialized a temporary pair, and ChNameValue stores only a raw pointer to its value (it takes const T& and keeps (T*)&mvalue). Two things followed: the archive wrote a copy of the element rather than the element itself, and that copy had already been destroyed by the time it was written, so the write read freed memory.
The visible effect depended on the value type. A trivially destructible type such as ChVector3d usually still read back its old bytes and looked correct, which is why this survived so long. A polymorphic type with a non-trivial destructor lost everything below its base class: serializing a std::map<std::string, ChLinkTSDA> produced a 254 byte record holding only the ChObj fields, with no _version_ChLinkTSDA, no spring coefficients and no bodies. Reported in #507, where it originally presented as a segmentation fault.
The fix declares the key const in the two out() wrappers, so the ChNameValue binds directly to the real map element and no temporary is created. The in() path is deliberately left alone: _wrap_pair::ArchiveIn writes into first, so a const key there would assign through a pointer to a const object. Because member functions of a class template are instantiated only when used, ArchiveIn is never instantiated on the const-keyed specialization, and only ArchiveOut, which merely reads first, is affected.
The naive alternative, giving the temporary a name so that it outlives the call, would compile and stop the crash but would keep serializing a copy. That is not equivalent: ChLink's copy constructor nulls both body pointers, so it would trade a loud bug for a silent one.
Adds five regression tests to utest_CH_archive. They assert that the archive does not copy the container element, which is deterministic, rather than asserting on how the resulting undefined behavior happens to manifest, which varies with type and optimizer. vector and list are included as controls. With the two-line change reverted, the map and unordered_map tests fail (one copy observed, and the value reads back as the test payload's destructor poison) while the vector and list controls still pass. This file previously had no STL container coverage at all, and the only map in demo_CH_archive is an unordered_map<int, double> of PODs with no assertions, which is why nothing in tree caught this.
Unrelated and pre-existing, noted here only because this fix makes it visible in serialized output: ChObj's copy constructor never assigns m_tag, so any copied or cloned Chrono object carries a garbage tag. Confirmed without any serialization involved, where a ChBody tagged 77 reports 32765 after a copy. That is left for a separate report.
Verified with clang (ROCm 7.1) on Windows, Release: all 10 core unit test binaries pass (44 cases) and demo_CH_archive still round-trips.