Skip to content

Hunting down a UB exploit under LTO lead to the following: - #1015

Merged
MichaelRawson merged 2 commits into
masterfrom
lto-mystery
Oct 11, 2026
Merged

MichaelRawson merged 2 commits into
masterfrom
lto-mystery

Conversation

@quickbeam123

Copy link
Copy Markdown
Collaborator
  • some defensive printing inside SubstitutionTree_impl (should be cheap)
  • similarly for SkipLists
  • reimplement of Coproduct, storing its payload in a union, not a reinterpreted byte buffer, which actually fixed the binary

A performance regression test is under way, which will also suggest how much LTO brings us, roughly.

@quickbeam123
quickbeam123 added this pull request to stack #1016 October 9, 2026 17:40

@MichaelRawson MichaelRawson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'd do the SkipList thing without a wider mandate for this kind of thing throughout. But it's a matter of taste, and the rest looks good. Thanks for hunting this nightmare bug!

Comment thread Lib/Coproduct.hpp Outdated
Comment thread Lib/Coproduct.hpp
Comment thread Lib/Coproduct.hpp Outdated
Comment thread Lib/SkipList.hpp Outdated
Base automatically changed from martin-polynf-odr to master October 10, 2026 12:49
quickbeam123 and others added 2 commits October 11, 2026 10:33
Coproduct kept the live alternative in `char _content[maxSize]` and read it
back through

  template<class B> B& cast() { return *(B*)_content; }

which every payload access in the class funnels through (unwrap, and so
compare, operator==, apply, operator<<). That is undefined twice over:

 - the returned pointer is derived from the char array, not from the object
   that reused its storage, which is exactly the case std::launder exists for;
 - the same bytes are written at one type (placement-new of the alternative,
   or a memberwise copy of the Coproduct) and read at another, which is the
   assumption -fstrict-aliasing is allowed to rely on being false.

Replaced by a recursive union, so the compiler is told the truth -- these types
share this memory -- and reading the live alternative is an ordinary member
access at the right type. No cast, nothing to launder, and the union supplies
the size and alignment, so MaxSize/MaxAlign are gone.

cast<B>() becomes castAt<idx>(). All five call sites already had the index in
hand, and going through it means the access path used to read an alternative is
the same path the placement-new constructed it through. It also removes a
latent conflation: with duplicate alternative types, cast<B>() resolved to the
first index of type B rather than to the index actually asked for.

VariantStorage has two specialisations, as a standard-library variant does.
Nothing is implicitly constructed or destroyed in the storage -- Coproduct does
both explicitly via TrivialOperations -- but the destructor has to stay
*trivial* when every alternative is trivially destructible, or Coproduct loses
its own triviality.

Layout is unchanged. Compiling the old and the new header side by side gives
sizeof 16 for Coproduct<VarNumber,SymbolId> and 24 for
Coproduct<int,double,char> either way, and the in-header
is_trivially_copyable<RawCoproduct<int,int>> assertion still holds, as do
is_trivially_copyable and is_trivially_destructible for a two-POD Coproduct and
!is_trivially_destructible for Coproduct<std::vector<int>,int>.

Found via an -flto=auto build of this branch, where

  vampire -sa otter Problems/SET/SET978+1.p

could not find a substitution tree child that was demonstrably present:
Top::compare, which reads the Coproduct payload, disagreed with the structure.
Putting __attribute__((optimize("no-strict-aliasing"))) on cast() fixed it, and
so did __attribute__((noinline)) alone -- the two preconditions of one
mechanism, inlining bringing the write and the read into a single body and TBAA
then being allowed to conclude they are unrelated.

Verified: ctest 103/103, and SET978+1, PUZ032-1, SYN084-1, SEU144+1, SWV019-1,
ANA019-2, GRA013+1 all answered correctly by a debug build.

Co-Authored-By: Claude Opus 5 <[email protected]>
@MichaelRawson
MichaelRawson merged commit 1db5462 into master Oct 11, 2026
1 check passed
@MichaelRawson
MichaelRawson deleted the lto-mystery branch October 11, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants