https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126408

            Bug ID: 126408
           Summary: false -Warray-bounds when speculative devirtualization
                    inlines the wrong target of a 2-target polymorphic
                    call
           Product: gcc
           Version: 16.1.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: liweifriends at gmail dot com
  Target Milestone: ---

GCC 16 emits a bogus -Warray-bounds on a code path that is unreachable at run
time. The path is created by ipa-devirt itself: a polymorphic call with exactly
two candidate targets is turned into a speculative 2-way dispatch, both
candidate
bodies are inlined under vtable guards, and the array-bounds pass then reports
member accesses in the branch belonging to the *other* (larger) type as being
out of bounds of the (smaller) object that the caller actually allocated.

GCC 15.3.1 accepts the same code silently. This is a 15 -> 16 regression.

Reduced from a real type-erasure ("polymorphic wrapper") library, where it
breaks
a -Werror build.

--------------------------------------------------------------------------------
REPRODUCER (self-contained, no headers beyond libstdc++)
--------------------------------------------------------------------------------
Save as bugreport.cpp. The line numbers quoted below assume the file starts at
the #include, with no leading comment.

#include <memory>
#include <utility>
#include <exception>

struct AbsImp {
    virtual ~AbsImp() = default;
    virtual bool is_eof() = 0;
    virtual int  bos()    = 0;
    virtual void to_get() = 0;
};

struct Small {
    char m_buf[64]{};
    bool m_eof = false;
    long m_pos = 0;
    bool is_eof() { return m_eof; }
    int  bos()    { return 1; }
    void to_get() { m_buf[2]++; }
};

template <typename K>
struct Wrap : K {
    char m_zbuf[256]{};
    bool m_ended = false;
    bool is_eof() { return m_ended || K::is_eof(); }
    int  bos()    { m_ended = false; return K::bos(); }
    void to_get() { m_zbuf[5]++; K::to_get(); }
};

template <typename K>
struct Imp : AbsImp {
    int m_state = 0;
    K   m_kernel;
    explicit Imp(K k) : m_kernel(std::move(k)) {}
    bool is_eof() override { return m_kernel.is_eof(); }
    int  bos()    override { m_state = m_kernel.bos(); return m_state; }
    void to_get() override { m_state = 2; m_kernel.to_get(); }
};

struct Erased {
    std::unique_ptr<AbsImp> m_ptr;
    template <typename K>
    explicit Erased(K k) : m_ptr(new Imp<K>(std::move(k))) {}

    bool is_eof() { if (!m_ptr) std::rethrow_exception(std::exception_ptr{});
return m_ptr->is_eof(); }
    int  bos()    { if (!m_ptr) std::rethrow_exception(std::exception_ptr{});
return m_ptr->bos(); }
    void to_get() { if (!m_ptr) std::rethrow_exception(std::exception_ptr{});
m_ptr->to_get(); }
};

extern void sink(long);

void small_ops() {
    Erased e{Small{}};
    e.bos();
    sink(e.is_eof());
    e.to_get();
}

void big_ops() {
    Erased e{Wrap<Small>{}};
    e.bos();
    e.to_get();
}

--------------------------------------------------------------------------------
COMMAND
--------------------------------------------------------------------------------

$ g++ -O2 -Wall -std=c++17 -c bugreport.cpp

--------------------------------------------------------------------------------
ACTUAL OUTPUT (GCC 16.1.1)
--------------------------------------------------------------------------------

In member function 'void Wrap<K>::to_get() [with K = Small]',
    inlined from 'void Imp<K>::to_get() [with K = Wrap<Small>]' at
bugreport.cpp:37:58,
    inlined from 'void Erased::to_get()' at bugreport.cpp:47:92,
    inlined from 'void small_ops()' at bugreport.cpp:56:13:
bugreport.cpp:27:29: warning: array subscript 'Wrap<Small>[0]' is partly
outside array bounds of 'unsigned char [96]' [-Warray-bounds=]
   27 |     void to_get() { m_zbuf[5]++; K::to_get(); }
      |                     ~~~~~~~~^
In constructor 'Erased::Erased(K) [with K = Small]',
    inlined from 'void small_ops()' at bugreport.cpp:53:21:
bugreport.cpp:43:34: note: at offset 16 into object of size 96 allocated by
'operator new'
   43 |     explicit Erased(K k) : m_ptr(new Imp<K>(std::move(k))) {}
      |                                  ^~~~~~~~~~~~~~~~~~~~~~~~

(a second, near-identical warning follows for column 21 of the same line)

--------------------------------------------------------------------------------
EXPECTED OUTPUT
--------------------------------------------------------------------------------

No diagnostic. GCC 15.3.1 compiles this silently.

--------------------------------------------------------------------------------
WHY THIS IS A FALSE POSITIVE
--------------------------------------------------------------------------------

Note the inlining chain in the diagnostic. It pairs

  - an allocation of Imp<Small>          (96 bytes, from small_ops), with
  - the body of Imp<Wrap<Small>>::to_get (a *different*, larger type).

Those two can never occur together at run time. In small_ops() the object is
constructed one line earlier by 'new Imp<Small>', so its dynamic type is
statically evident; Imp<Wrap<Small>> is only ever created in big_ops().

The offending path is introduced by ipa-devirt. -fdump-ipa-devirt shows the
call
site being given two speculative targets:

  bugreport.cpp:47:92: optimized: speculatively devirtualizing call in
      void Erased::to_get()/300 to void Imp<K>::to_get() [with K = Small]/465
  bugreport.cpp:47:92: optimized: speculatively devirtualizing call in
      void Erased::to_get()/300 to void Imp<K>::to_get() [with K =
Wrap<Small>]/462
  bugreport.cpp:47:92: optimized: devirtualized call in
      void Erased::to_get()/300 to 2 targets

Erased is deliberately *not* templated on the kernel type, so a single
Erased::to_get() body is shared by both instantiations. ipa-devirt sees exactly
two candidates there and emits a speculative 2-way dispatch: both bodies
inlined,
each guarded by a vtable check. That combined body is subsequently inlined into
small_ops(), where the allocation size is known to be 96. Inside the
Wrap<Small> arm -- which the vtable guard can never select for an Imp<Small>
object -- m_zbuf sits past offset 96, and -Warray-bounds flags it.

So the access is unreachable, not out of bounds. The array-bounds pass does not
appear to account for the vtable guard that ipa-devirt placed around the
speculative branch.

Linking the TU against a driver that calls small_ops()/big_ops() and defines
sink() produces a program that runs correctly; the flagged access never
executes.

--------------------------------------------------------------------------------
CONDITIONS
--------------------------------------------------------------------------------

* Regression: GCC 15.3.1 clean, GCC 16.1.1 warns.
* -O0 and -O1 are clean; -O2 and -O3 warn.
* Independent of -std: c++11/14/17/20/23 all reproduce.
* Requires *exactly two* candidate targets at the call site. With one, GCC
  devirtualizes exactly and there is no warning.
* main() must live in a different TU. Adding main() to this file lets GCC see
  the concrete types program-wide, devirtualize exactly, and the warning goes
  away -- which is itself further evidence that the speculation is the cause.
* On the original (unreduced) code, -O3 produced noticeably more of these
  warnings than -O2, so the problem scales with how aggressively the
speculative
  dispatch is applied.

--------------------------------------------------------------------------------
WORKAROUND
--------------------------------------------------------------------------------

-fno-devirtualize-speculatively suppresses it (as does -fno-devirtualize),
which
confirms the pass responsible:

$ g++ -O2 -Wall -std=c++17 -c bugreport.cpp -fno-devirtualize-speculatively
(no output)

--------------------------------------------------------------------------------
VERSION
--------------------------------------------------------------------------------

$ g++ --version
g++ (GCC) 16.1.1 20260515 (Red Hat 16.1.1-2)

$ g++-15 --version
g++-15 (GCC) 15.3.1 20260722 (Red Hat 15.3.1-1)

Host and target: x86_64-redhat-linux (Fedora)
  • [Bug c++/126408] New: false -Wa... liweifriends at gmail dot com via Gcc-bugs

Reply via email to