On 9/19/26 22:27, Siddhesh Poyarekar wrote:
On 2026-09-19 00:22, Kees Cook wrote:
On Thu, Sep 03, 2026 at 08:02:20PM -0600, Gustavo A. R. Silva wrote:
For a pointer to a subobject whose record/union type ends in a flexible-array
member (directly, or through a trailing nested struct), addr_object_size()
walked up to the enclosing object (v = TREE_OPERAND (v, 0)) instead of
measuring the referenced subobject. __builtin_object_size() and
__builtin_dynamic_object_size() type 1 therefore returned the whole-object
size, collapsing type 1 onto type 0 and losing the distinction between
&p->inner and p. FORTIFY_SOURCE relies on the type-1 distinction, so this
weakens its bounds checks.
Looking at the PR:
struct flex {
size_t count;
char fam[] __attribute__((counted_by(count)));
}; /* sizeof(struct flex) == 8 */
struct outer {
int hdr;
struct flex inner;
}; /* sizeof(struct outer) == 16 */
int main (void)
{
struct outer *p = __builtin_malloc (sizeof (*p) + 48); /* 64-byte object */
/* This is a bug. */
expect(__builtin_object_size (&p->inner, 1), sizeof(p->inner));
expect(__builtin_dynamic_object_size (&p->inner, 1), sizeof(p->inner));
...
}
Before:
WAT: __builtin_object_size (&p->inner, 1) == 56 (expected 8)
WAT: __builtin_dynamic_object_size (&p->inner, 1) == 56 (expected 8)
After:
ok: __builtin_object_size (&p->inner, 1) == 8
ok: __builtin_dynamic_object_size (&p->inner, 1) == 8
This makes sense to me, the dynamic size of "fam" is ambiguous between
being 0 or 48 (from the "alloc_size" attribute), so type 1 chooses the
smaller.
I think you're confusing type 1 with type 2; type 1 is the *maximum* subobject size, not the minimum. The whole object and subobject size distinction is only
material if there are members after inner, which is technically "supported" by the complier as an extension, but is a terrible idea for members with FAM.
Here are the different possibilities for &p->inner:
1. If the allocation via __builtin_malloc is visible: same result for types 0,
1, 2 and 3, i.e. allocated size - offsetof (struct outer, inner)
When the allocation is visible and the FAM is annotated with counted_by, we use
the information provided by counted_by. See commit 6f17933548fc ("Use the
.ACCESS_WITH_SIZE in builtin object size.")
BTW, I recently filed this related issue:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127234
2. If the __builtin_malloc is not visible and there's no counted_by annotation:
- type 0 and type 1: cannot determine size, since FAM could be any arbitrary
size. So, -1
- type 2 and type 3: sizeof (&p->inner), since that's the minimum estimate.
3. if the __builtin_malloc is not visible and FAM has __counted_by__ annotation: same
result for all types: sizeof (&p->inner) + p->count
I would, however, expect the use of counted_by to change it. For
example, if it were this:
struct flex {
size_t count;
char fam[] __attribute__((counted_by(count)));
};
...
struct outer *p = __builtin_malloc (sizeof (*p) + 48); /* 64-byte object */
p->inner.count = 48;
I would expect the results to be:
ok: __builtin_object_size (&p->inner, 1) == 8 /* compile-time size */
ok: __builtin_dynamic_object_size (&p->inner, 1) == 56 /* run-time size */
I agree with counted_by changing things here. This is actually something
that Jakub mentioned in a comment to v1 of this patch:
"What we certainly can change is the behavior when
-fstrict-flex-arrays unless it already behaves the expected way,
and perhaps also when the flex array has counted_by attribute."
See: https://lore.kernel.org/linux-hardening/aogVH8UtICMP1Ihv@tucnak/#t
BTW, I have a patch ready for PR127234 (the missed counted_by bug) where
I implemented some helper functions to get the size of the object based
on the information provided by counted_by. I could use those in this PR. :)
Thanks
-Gustavo
As the known size of p->inner is everything contained by "inner" and
that now includes the 48 bytes of "fam".
__builtin_object_size is not the compile time size, it is a runtime constant size estimate, with the `type` argument deciding if the size estimate is on the
maximum or minimum end, and if it considers the whole object or only the immediate containing subobject that the input pointer points to. In the presence of
FAMs I think sizeof (i.e. the compile time size) should be seen as a minimum size estimate for the subobject, if you want to make an equivalence with
__builtin_object_size.
Similarly, __builtin_dynamic_object_size is exactly the same as
__builtin_object_size, except that it can return non-constant expressions too,
not just constants.
Thanks,
Sid