https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105684
Bug ID: 105684 Summary: Bogus `-Warray-bounds` in partially allocated struct Product: gcc Version: 12.1.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: c Assignee: unassigned at gcc dot gnu.org Reporter: sagebar at web dot de Target Milestone: --- Accessing the fields of a partially allocated struct results in `-Warray-bounds`, even when all accessed fields lie within the allocated portion of the struct (a fact that can be proven to be known to gcc by asking it for `__builtin_object_size()`) Example (compile with `gcc -c infile.c -O2 -Warray-bounds -S -o -`): ``` struct obj { unsigned long long kind; union { struct { unsigned long long a; } data0; struct { unsigned long long a; unsigned long long b; } data1; }; }; extern __attribute__((alloc_size(1))) void *my_malloc(unsigned long); #define offsetafter(s, m) (__builtin_offsetof(s, m) + sizeof(((s *)0)->m)) struct obj *create_object_kind_0(void) { struct obj *result; result = (struct obj *)my_malloc(offsetafter(struct obj, data0)); __asm__ __volatile__("# Object size is: %p0" : : "X" (__builtin_object_size(result, 0))); result->kind = 0; result->data0.a = 1; return result; } ``` Real-world use-case & rationale: the idea is that access to `data1` vs. `data0` is governed by the value of `kind` (`obj` is a "typed variant"), and by allocating only the relevant portion of the object, we can save memory, and create an environment where buggy code that wrongfully accesses (e.g.) certain fields of `data1` of a `kind=0` object might cause the program to crash (rather than remain unnoticed as would be the case when simply using `sizeof(struct obj)` as allocation size). Expected behavior: no warnings should be generated ====== Actual behavior (Truncated) ======= >[stderr]infile.c: In function 'create_object_kind_0': >[stderr]infile.c:22:15: warning: array subscript 'struct obj[0]' is partly >outside array bounds of 'unsigned char[16]' [-Warray-bounds] >[stderr] 21 | result->kind = 0; >[stderr] | ^~ >[stderr]infile.c:20:32: note: object of size 16 allocated by 'my_malloc' >[stderr] 19 | result = (struct obj *)my_malloc(offsetafter(struct >obj, data0)); >[stderr] | >^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >[stderr]infile.c:23:15: warning: array subscript 'struct obj[0]' is partly >outside array bounds of 'unsigned char[16]' [-Warray-bounds] >[stderr] 22 | result->data0.a = 1; >[stderr] | ^~ >[stderr]infile.c:20:32: note: object of size 16 allocated by 'my_malloc' >[stderr] 19 | result = (struct obj *)my_malloc(offsetafter(struct >obj, data0)); >[stderr] | >^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >[stdout][...] >[stdout]/ 22 "infile.c" 1 >[stdout] # Object size is: 16 >[stdout]/ 0 "" 2 >[stdout]/NO_APP >[stdout] movl $0, (%eax) >[stdout] movl $0, 4(%eax) >[stdout] movl $1, 8(%eax) >[stdout] movl $0, 12(%eax) >[stdout] leave >[stdout] ret >[stdout][...] As can be seen by `# Object size is: 16` (and repeated in warning messages), gcc knows that the allocated size of the object is 16 bytes, and as can be seen by the offsets used by the `movl` instructions, this limit is never exceeded. Judging by what the warnings state (and confirmed by replacing `offsetafter(struct obj, data0)` with `sizeof(struct obj)`), the `-Warray-bounds` warning will always be generated for any accessed field (irregardless of that field's offset) so-long as `__builtin_object_size(result, 0) < sizeof(struct obj)`. I am uncertain about the intended semantics of `-Warray-bounds`, and technically speaking, the warning is stating the truth: "'struct obj[0]' is partly outside array bounds" (emphasis on the "partly"). However, as it stands right now, warning about this situation is less than useful (a warning might arguably be useful when passing `result` -- or a pointer to one of its members -- to some other function, but even in that situation a warning would probably be a false positive). So in my opinion, at `-Warray-bounds[=1]`, a warning should only be generated exactly in those cases where an out-of-bounds access *actually* happens (*possible* out-of-bounds warnings may appear at `-Warray-bounds=2`, but definitely shouldn't at `-Warray-bounds=1`). ================================================== Additionally, after making the following changes to the above code (causing `sizeof(struct obj) == offsetafter(struct obj, data0)`): ``` @@ -6,7 +6,7 @@ } data0; struct { unsigned long long a; - unsigned long long b; + unsigned long long b[]; } data1; }; }; @@ -20,6 +20,7 @@ __asm__ __volatile__("# Object size is: %p0" : : "X" (__builtin_object_size(result, 0))); result->kind = 0; result->data0.a = 1; + result->data1.b[0] = 1; return result; } ``` Expected behavior: A warnings should be generated for `result->data1.b[0] = 1` ====== Actual behavior ======= No warnings are generated, even though **now** an out-of-bounds access actually **does** take place (`result->data1.b[0] = 1`), and gcc should easily be able to spot this (see suggestion below). ====== Conclusion ======= It would seem that an expression like `obj-><FIELDEXPR> = v` only causes gcc to make the following check: > if (__builtin_object_size(obj, 0) < sizeof(typeof(*obj))) > WARN("-Warray-bounds"); Rather than checking (suggestion): > if (__builtin_object_size(obj, 0) < offsetafter(typeof(*obj), <FIELDEXPR>)) > WARN("-Warray-bounds"); The former obviously breaks for partially allocated structs while not producing any warning for out-of-bounds access to a flexible array member (since sizeof() on such an object only returns the offset of the flexible array member). However, if instead the suggested check were to be made, the false warnings wouldn't appear, and the currently missing warning would be generated.