Skip to content

Py_BUILD_ASSERT is broken on non-constant expression #118124

Closed
@namniav

Description

@namniav

Bug report

Bug description:

Macros Py_BUILD_ASSERT and Py_BUILD_ASSERT_EXPR defined in Include/pymacro.h are broken when cond is not a constant expression, because sizeof is allowed to apply on variable-length arrays(VLA) since C99 and with compiler extension since C89.

/* Assert a build-time dependency, as an expression.

   Your compile will fail if the condition isn't true, or can't be evaluated
   by the compiler. This can be used in an expression: its value is 0.

   Example:

   #define foo_to_char(foo)  \
       ((char *)(foo)        \
        + Py_BUILD_ASSERT_EXPR(offsetof(struct foo, string) == 0))

   Written by Rusty Russell, public domain, http://ccodearchive.net/ */
#define Py_BUILD_ASSERT_EXPR(cond) \
    (sizeof(char [1 - 2*!(cond)]) - 1)
 
#define Py_BUILD_ASSERT(cond)  do {         \
        (void)Py_BUILD_ASSERT_EXPR(cond);   \
    } while(0)

Following code compiles on Clang and GCC without error:

void foo(int param)
{
    Py_BUILD_ASSERT(param > 0);
}

Since CPython is now using C11 (PEP7) and static_assert is already used in other public internal headers(e.g. Include/internal/pycore_long.h), Py_BUILD_ASSERT should be deprecated and use static_assert instead.

Since they are defined in a public header of CPython without a "_Py" prefix, removing them might break third-party code.

Py_BUILD_ASSERT can be defined to static_assert in a way with deprecating warning message:

#define Py_BUILD_ASSERT(cond)  do { \
        Py_DEPRECATED(3.14) \
        int _Py_BUILD_ASSERT_is_deprecated_use_static_assert_instead = 0; \
        _Py_BUILD_ASSERT_is_deprecated_use_static_assert_instead; \
        static_assert(cond, ""); \
    } while(0)

Py_BUILD_ASSERT_EXPR is used (and only used) in another macro Py_ARRAY_LENGTH, so it can't be deprecated yet.

Fixing Py_BUILD_ASSERT_EXPR is tricky, most because of old non-conformant MSVC1. And we need it also working in C++. The easiest fix is to change documentation so that it should be used only with constant expression or there's no assertion otherwise. But If we want to make it mandatory, here's a workaround:

#if defined(__cplusplus)
    template<typename T>
    struct _Py_BUILD_ASSERT_EXPR_prohibit_vla {
        static_assert(sizeof(T) == 1,
            "Py_BUILD_ASSERT_EXPR can only be used with constant "
            "expression of value true");
    };
#  define Py_BUILD_ASSERT_EXPR(cond) \
        (!sizeof(_Py_BUILD_ASSERT_EXPR_prohibit_vla<char[1 - 2 * !(cond)]>))
#elif defined(_MSC_VER)
#  define Py_BUILD_ASSERT_EXPR(cond) \
        (!sizeof( \
            __pragma(warning(push)) \
            __pragma(warning(suppress: 4116)) \
            enum { \
                Py_CONCAT(_Py_BUILD_ASSERT_EXPR_prohibit_vla_,__LINE__) = \
                    sizeof(char[1 - 2 * !(cond)]) \
            } \
            __pragma(warning(pop)) \
        ))
#else
#  define Py_BUILD_ASSERT_EXPR(cond) \
        (!sizeof( \
            enum { \
                Py_CONCAT(_Py_BUILD_ASSERT_EXPR_prohibit_vla_,__LINE__) = \
                    sizeof(char[1 - 2 * !(cond)]) \
            } \
        ))
#endif

You can view and try the workarounds using the amazing conformance-viewer in amazing Compiler Explorer: for C and for C++.

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Linked PRs

Footnotes

  1. One of the unbelievable bugs is that MSVC prior to v19.21 accepts negative-length(implicitly extended to unsigned value) arrays when used as template argument, e.g. A<char[-1]>.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions