https://gcc.gnu.org/bugzilla/show_bug.cgi?id=103964
Bug ID: 103964
Summary: [9/10/11/12 Regression] OVS miscompilation since
r0-92313-g5006671f1aaa63cd
Product: gcc
Version: 12.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: tree-optimization
Assignee: unassigned at gcc dot gnu.org
Reporter: jakub at gcc dot gnu.org
Target Milestone: ---
Following testcase is miscompiled e.g. with -g -Og -fno-strict-aliasing
starting with r0-92313-g5006671f1aaa63cd - this is a self-contained testcase
from https://gcc.gnu.org/pipermail/gcc-help/2021-December/141021.html
With current trunk and -g -Og -fno-strict-aliasing (-Og chosen as something
that does just a few optimizations), I see on current gcc trunk the fre1 pass
optimizing:
- _69 = start.prev;
# DEBUG list_ => NULL
- last_49 = _69 + 18446744073709551552;
- # DEBUG last => last_49
+ # DEBUG last => &MEM <struct ovs_list> [(void *)&start + -64B]
# DEBUG BEGIN_STMT
- printf ("first: %p \nlast: %p\n", first_47, last_49);
+ printf ("first: %p \nlast: %p\n", first_47, &MEM <struct ovs_list> [(void
*)&start + -64B]);
We have earlier:
start.prev = &start;
start.next = &start;
and .prev stores in between are:
MEM[(struct ovs_list *)member_59 + 64B].prev = _48;
...
MEM[(struct ovs_list *)pos_32 + 64B].prev = _15;
I bet the alias oracle assumes that pos_32, being an struct member pointer,
can't overwrite start.prev where start is much smaller than that (has just
struct ovs_list type).
That MEM[(struct ovs_list *)pos_32 + 64B].prev = _15; is actually what
overwrites start.prev.
# pos_32 = PHI <pos_61(8), pos_62(10)>
and
_6 = start.next;
_7 = (long unsigned int) _6;
_8 = _7 + 18446744073709551552;
pos_61 = (struct member *) _8;
and
_11 = pos_32->elem.next;
_12 = (long unsigned int) _11;
_13 = _12 + 18446744073709551552;
pos_62 = (struct member *) _13;
If it wouldn't use uintptr_t in there, I'd say it is clearly UB, doing pointer
arithmetics out of bounds of the start object. With uintptr_t it just
materializes a pointer known to point outside of the start object.
For -fstrict-aliasing, I think it is just fine to treat it as UB, for
-fno-strict-aliasing I don't know.
I'm afraid Linux kernel and various other projects that copied such
questionable code from it use it heavily.
/* How to build:
>> gcc -O2 -g -o example2 example2.c ^C
>> ./example2
start: 0x7ffc13ba2800
first: 0x1ba32f0
last: 0x7ffc13ba27c0
list is broken!
Start: 0x7ffc13ba2800. start->next: 0x1ba3330, start->next->next: 0x1ba3390,
start->prev: 0x1ba3390
>> gcc -g -o example2 example2.c
>> ./example2
start: 0x7ffd84d91660
first: 0x23b52f0
last: 0x23b5350
Same for clang.
*/
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
////////////////// from include/openvswitch/util.h //////////////////
#define INIT_CONTAINER(OBJECT, POINTER, MEMBER)
\
((OBJECT) = NULL, ASSIGN_CONTAINER(OBJECT, POINTER, MEMBER))
#define OVS_TYPEOF(OBJECT) typeof(OBJECT)
#define OBJECT_OFFSETOF(OBJECT, MEMBER) offsetof(typeof(*(OBJECT)), MEMBER)
#define OBJECT_CONTAINING(POINTER, OBJECT, MEMBER)
\
((OVS_TYPEOF(OBJECT))(
\
void*)((char*)(POINTER)-OBJECT_OFFSETOF(OBJECT, MEMBER)))
#define OBJECT_MEMBER(POINTER, OBJECT, MEMBER)
\
((OVS_TYPEOF(&POINTER->MEMBER))
\
((uintptr_t) POINTER + OBJECT_OFFSETOF(POINTER, MEMBER)))
#define ASSIGN_CONTAINER(OBJECT, POINTER, MEMBER)
\
((OBJECT) = OBJECT_CONTAINING(POINTER, OBJECT, MEMBER), (void)0)
#define HMAP_FOR_EACH(NODE, MEMBER, HMAP)
\
HMAP_FOR_EACH_INIT(NODE, MEMBER, HMAP, (void)0)
#define HMAP_FOR_EACH_INIT(NODE, MEMBER, HMAP, ...)
\
for (INIT_CONTAINER(NODE, hmap_first(HMAP), MEMBER), __VA_ARGS__;
\
(NODE != OBJECT_CONTAINING(NULL, NODE, MEMBER)) ||
\
((NODE = NULL), 0);
\
ASSIGN_CONTAINER(NODE, hmap_next(HMAP, &(NODE)->MEMBER), MEMBER))
#define LIST_FOR_EACH(ITER, MEMBER, LIST)
\
for (INIT_CONTAINER(ITER, (LIST)->next, MEMBER);
\
&(ITER)->MEMBER != (LIST);
\
ASSIGN_CONTAINER(ITER, (ITER)->MEMBER.next, MEMBER))
////////////// from lib/list.c / include/openvswitch/list.h /////////////////
struct ovs_list {
struct ovs_list* prev; /* Previous list element. */
struct ovs_list* next; /* Next list element. */
};
static inline void ovs_list_init(struct ovs_list* list)
{
list->next = list->prev = list;
}
static inline void ovs_list_insert(struct ovs_list* before,
struct ovs_list* elem)
{
elem->prev = before->prev;
elem->next = before;
before->prev->next = elem;
before->prev = elem;
}
#define CONTAINER_OF(POINTER, STRUCT, MEMBER)
\
((STRUCT*)(void*)((char*)(POINTER)-offsetof(STRUCT, MEMBER)))
#define CONST_CAST(TYPE, POINTER) ((TYPE)(POINTER))
static inline struct ovs_list* ovs_list_front(const struct ovs_list* list_)
{
struct ovs_list* list = CONST_CAST(struct ovs_list*, list_);
// ovs_assert(!ovs_list_is_empty(list));
return list->next;
}
/* Returns the back element in 'list_'.
Undefined behavior if 'list_' is empty. */
static inline struct ovs_list* ovs_list_back(const struct ovs_list* list_)
{
struct ovs_list* list = CONST_CAST(struct ovs_list*, list_);
// ovs_assert(!ovs_list_is_empty(list));
return list->prev;
}
////////////////////////////////////////////////////////////////////////////////
struct member {
int padding[14];
int order;
struct ovs_list elem;
};
int main()
{
int i, ret = 0;
struct member *member, *members[2];
/*
TESTED: If i create the struct ovs_list in the heap and
change the rest of the program to use start instead of &start,
the problem disappears
struct ovs_list *start = malloc(sizeof (struct ovs_list));
if (start) {
memset(start, 0, sizeof (struct bond));
}
TESTED: If I create an entire struct member and I use it's embedded
ovs_list
(ensuring we have enough stack space so that any pointer arithmetics will
always fall into addressable area), the problem dissapears
struct member sstart = {};
struct ovs_list *start = &sstart.elem;
*/
struct ovs_list start = {0};
// Boilerplate initialization
for (i = 0; i < 2; i++) {
members[i] = malloc(sizeof(struct member));
if (members[i]) {
memset(members[i], 0, sizeof(struct member));
}
}
// Set arbitrary order
members[0]->order = 3;
members[1]->order = 2;
printf("start: %p\n", &start);
ovs_list_init(&start);
/* Original code.
HMAP_FOR_EACH (member, hmap_node, &bond->members) {
struct member *pos;
LIST_FOR_EACH (pos, elem, &start) {
if (member->order > pos->order) {
break;
}
}
//printf("Inserting member: %p\n", member);
ovs_list_insert(&pos->elem, &member->elem);
}
What follows is the same code with the macros expanded.
*/
for (i = 0; i < 2; i++) {
struct member* pos;
member = members[i];
/* LIST_FOR_EACH (pos, elem, &start) {
* 'char *' casts replaced with 'uintptr_t' to make clang also fail. */
for (((pos) = ((void*)0),
((pos) = ((typeof(pos))(void*)((uintptr_t)((&start)->next) -
__builtin_offsetof(struct member,
elem)))));
&(pos)->elem != (&start);
// Alternative ways to check:
//(uintptr_t) pos + __builtin_offsetof(struct member, elem) !=
(uintptr_t)(&start);
//OBJECT_MEMBER(pos, struct member, elem) != (&start);
((pos) = ((typeof(pos))(void*)((uintptr_t)((pos)->elem.next) -
__builtin_offsetof(struct member,
elem))))) {
if (member->order > pos->order) {
break;
}
}
// TESTED: If I add this printf, the problem disappears
//printf("pos: %p\n", pos);
// Original version of the code. Doesn't work:
ovs_list_insert(&pos->elem, &member->elem);
// TESTED: This works:
//ovs_list_insert((void *)((uintptr_t)pos + __builtin_offsetof(struct
member,elem)), &member->elem);
// TESTED: This doesn't work:
//ovs_list_insert((void *)((char * )pos + __builtin_offsetof(struct
member,elem)), &member->elem);
// TESTED: This also work:
//ovs_list_insert((void *)((uintptr_t)pos + 64), &member->elem);
// TESTED: This doesn't:
//ovs_list_insert((void *)((char * )pos + 64), &member->elem);
// Just a prettier wrapper for __builtin_offsetof math. Works:
//ovs_list_insert(OBJECT_MEMBER(pos, struct member, elem),
&member->elem);
}
struct member* first =
CONTAINER_OF(ovs_list_front(&start), struct member, elem);
struct member* last =
CONTAINER_OF(ovs_list_back(&start), struct member, elem);
printf("first: %p \nlast: %p\n", first, last);
/* I've inserted two members into the 'start' list.
* first and last have to be either member1 or member2
* */
if ((first != members[0] && first != members[1]) ||
(last != members[0] && last != members[1])) {
printf("list is broken!\n");
printf("Start: %p. start->next: %p, start->next->next: %p, "
"start->prev: %p\n",
&start, start.next, start.next->next, start.prev);
ret = 1;
goto out;
}
out:
// cleanup
free(members[0]);
free(members[1]);
return ret;
}