https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70147
--- Comment #23 from Jakub Jelinek <jakub at gcc dot gnu.org> --- Consider: // PR c++/70147 // { dg-do run } // { dg-options "-fsanitize=vptr" } static int ac, ad, bc, bd, cc, cd, dc, dd; struct A { A () { ac++; } virtual void f () {} ~A () { ad++; }; }; struct D { D (int x) { if (x) throw 1; dc++; } ~D () { dd++; } }; struct B : virtual A, D { B () : D (1) { bc++; } virtual void f () {} ~B () { bd++; } }; struct C : B, virtual A { C () { cc++; } ~C () { cd++; } }; int main () { { try { C c; } catch (...) {} } if (ac != 1 || ad != 1 || bc || bd || cc || cd || dc || dd) __builtin_abort (); } This shows that for -fsanitize=vptr this PR is still not resolved, maybe all the sanitization vptr initializations to NULL need to be guarded with <NE_EXPR current_in_charge_parm, integer_zero_node> if there are any virtual bases, rather than just the initializers of the virtual base vtbl pointers. But, this also shows the CLOBBER issue. Above, we have 8 byte long CLOBBER in A::A, B::B and C::C and 0 byte long CLOBBER in D::D, the whole C is just 8 bytes long. C::C first clobbers all the 8 bytes (ok), then constructs A::A at the same address (this clobbers all of the 8 bytes (ok again), and sets the (only) pointer in there to &_ZTV1A + 16 (ok), then calls B::B subobject ctor on the same address, and this clobbers 8 bytes at that address again (wrong!, we rely on the earlier value of the vtable from the A::A ctor), then calls D::D which throws. Now, if say we'd e.g. inline the A::A and B::B ctors into C::C, but not D::D, I think DSE could happily remove the store of &_ZTV1A + 16 into _vptr.A. If I modify the testcase to: static int ac, ad, bc, bd, cc, cd, dc, dd; struct A { A () { ac++; } virtual void f () {} __attribute__((noinline)) ~A (); }; struct D { __attribute__((noinline)) D (int); ~D () { dd++; } }; struct B : virtual A, D { B () : D (1) { bc++; } virtual void f () {} ~B () { bd++; } }; struct C : B, virtual A { C () { cc++; } ~C () { cd++; } }; D::D (int x) { if (x) throw 1; dc++; } __attribute__((noinline, noclone)) void foo (A *p) { p->f (); } A::~A () { foo (this); ad++; } int main () { { try { C c; } catch (...) {} } if (ac != 1 || ad != 1 || bc || bd || cc || cd || dc || dd) __builtin_abort (); } then indeed I see e.g. in phicprop1: MEM[(struct &)&c] ={v} {CLOBBER}; MEM[(struct A *)&c]._vptr.A = &MEM[(void *)&_ZTV1A + 16B]; ac.11_20 = ac; _21 = ac.11_20 + 1; ac = _21; MEM[(struct &)&c] ={v} {CLOBBER}; D::D (&c.D.2413, 1); and in the following dse2: ac.11_20 = ac; _21 = ac.11_20 + 1; ac = _21; MEM[(struct &)&c] ={v} {CLOBBER}; D::D (&c.D.2413, 1); so the _vptr.A initialization is gone. The reason why this testcase doesn't abort is that the destructor A::~A as the first thing it does is it changes the _vptr.A again to &_ZTV1A + 16B. Are so problematic only empty classes with virtual bases, or when exactly can this happen? When I've added fields to all 4, A would not overlap with B and this wouldn't be an issue.