[Bug sanitizer/70147] [6 Regression] testcase from hana testsuite gets miscompiled with -fsanitize=undefined

jakub at gcc dot gnu.org gcc-bugzilla@gcc.gnu.org
Wed Mar 16 11:58:00 GMT 2016


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.


More information about the Gcc-bugs mailing list