Description
This is from ClangBuiltLinux/linux#2076 where commit 2d1e8a0 seems to have exposed some recent Linux kernel code to the loop unroller (though oddly only on ARM). Attempting to extract a minimal reproducer has been a challenge, but it appears that something about loop unrolling confuses __builtin_constant_p()
. Loop variables assigned from __builtin_dynamic_object_size()
appear constant, whereas if it is left in a macro, they correctly stay dynamic.
It seems that a noreturn
branch is also be required to trigger this. Anything that disables the loop unrolling fixes the problem (via #pragma
or via changes in loop size via NUM_ADDRS
). Bringing the external loop constraint explicitly down into the inner loop (j < NUM_ADDRS
) also fixes it.
I'm not sure what to do next to narrow this down further.
// clang -Wall -O2 -c repro.c -o repro.o
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#define __compiletime_warning(msg) __attribute__((__warning__(msg)))
#define __noreturn __attribute__((__noreturn__))
extern void variable_calc_constant(size_t) __compiletime_warning("variable calculation is constant");
extern void macro_calc_constant(size_t) __compiletime_warning("macro calculation is constant");
extern void freakout(void) __noreturn;
extern int check(int);
extern int unknown;
// must be less than 10 (loop unrolling limit?)
#define NUM_ADDRS 4
struct inner {
unsigned char addr[2]; // must be greater than 1 to show warnings in unroll
};
struct middle {
struct inner bytes;
};
struct outer {
struct middle array[NUM_ADDRS];
};
void repro(void) {
struct outer *outer;
struct middle *middle;
int i, c;
outer = malloc(sizeof(*outer));
middle = outer->array;
for (i = 0, c = 0; i < unknown && c < NUM_ADDRS; i++) {
struct inner addr = { };
int j;
if (check(i))
continue;
//#pragma clang loop unroll(disable)
for (j = 0; j < c /*&& j < NUM_ADDRS*/; j++) {
const size_t v_size = __builtin_dynamic_object_size(&middle[j].bytes, 0);
#define m_size __builtin_dynamic_object_size(&middle[j].bytes, 0)
if (__builtin_constant_p(v_size))
variable_calc_constant(v_size);
if (__builtin_constant_p(m_size))
macro_calc_constant(m_size);
if (m_size < sizeof(addr))
freakout();
if (__builtin_memcmp(&middle[j].bytes, &addr, sizeof(addr)) == 0)
break;
}
if (j == c)
c++;
}
}
Produces (on x86_64 host, FWIW):
$ clang -Wall -O2 -c repro.c -o repro.o
repro.c:53:5: warning: call to 'variable_calc_constant' declared with 'warning' attribute: variable calculation is constant [-Wattribute-warning]
53 | variable_calc_constant();
| ^
repro.c:53:5: warning: call to 'variable_calc_constant' declared with 'warning' attribute: variable calculation is constant [-Wattribute-warning]
repro.c:53:5: warning: call to 'variable_calc_constant' declared with 'warning' attribute: variable calculation is constant [-Wattribute-warning]
repro.c:53:5: warning: call to 'variable_calc_constant' declared with 'warning' attribute: variable calculation is constant [-Wattribute-warning]
repro.c:53:5: warning: call to 'variable_calc_constant' declared with 'warning' attribute: variable calculation is constant [-Wattribute-warning]
5 warnings generated.
Without loop unrolling, this does not emit warnings. The macro-based uses of __builtin_dynamic_object_size()
aren't resolved (which itself seems to be a bug), but the variable based ones are. Specifically, when __builtin_dynamic_object_size()
is being determined at compile time as part of the loop unrolling, it cannot reach 0 -- the loop unroller is exceeding the maximum loop counter and producing an iteration beyond the bounds of the array. This can be seen in the fifth (impossible) call in the binary output:
xor edi, edi
call variable_calc_constant@PLT