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)
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 */
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