> On Jun 18, 2025, at 12:17, Siddhesh Poyarekar <[email protected]> wrote:
>
> On 2025-06-18 10:32, Qing Zhao wrote:
>>> A NULL check alone does not solve this problem because the pointer could be
>>> arbitrary and invalid. I thought some more about adding the size
>>> expression only if there's an existing access like I suggested in that
>>> email, but I realize that doesn't make sense either, since code that uses
>>> __bdos typically would look to the __bdos result to determine validity of
>>> the access.
>>>
>>> Can you see a way to validate the pointer in addition to the NULL check?
>> Right now, we only generate the size expression for the following cases:
>> 1. When the pointer is read as the first parameter of the
>> __builtin_dynamic_object_size (p, 0);
>> AND
>> 2. p != NULL.
>> Is the above 1 & 2 not enough to avoid the introduced undefined behavior?
>> I thought that the above 1 should validate the pointer already, let me know
>> if this is not the case.
>> If So, could you please provide me a small testing case that illustrates the
>> issue?
>
> I'm afraid not, a NULL pointer is not the only case where a pointer could be
> invalid. For example, the pointer could have been recently freed or even be
> an arbitrary non-NULL value due to a memory corruption. Lastly, the memory
> location could be protected (e.g. PROT_NONE) which makes reading it an
> invalid operation.
>
> For example if you have:
>
> struct fam_container
> {
> int a;
> int count;
> char fam[] __counted_by__ (count);
> };
>
> void
> baz (struct fam_container *ptr)
> {
> ...
> ptr->count = 0;
> __builtin_free (ptr);
> ...
> }
>
> void
> bar (size_t insz)
> {
> struct fam_container *c =
> __builtin_malloc (insz + sizeof (struct fam_container));
> c->count = insz;
> baz (c);
> foo (c);
> }
>
> void
> __attribute__ ((noinline))
> foo (struct fam_container *ptr)
> {
> ...
> objsz = __builtin_dynamic_object_size (ptr, 0);
> __memcpy_chk (ptr, src, sz, objsz);
> ...
> }
>
> The function foo would transform to something like:
>
> void
> foo (struct fam_container *ptr)
> {
> ...
> objsz = ptr != NULL ? ptr->count + sizeof (*ptr) : -1;
> __memcpy_chk (ptr, src, sz, objsz);
> }
>
> Here, ptr is not NULL and tree-object-size ends up generating a dereference
> of a freed pointer, thus generating new undefined behaviour. Earlier too the
> program was buggy, i.e. __memcpy_chk was being passed a freed PTR, but
> tree-object-size ends up adding yet another invalid access BEFORE that one.
>
> The example above is a simplification of how _FORTIFY_SOURCE works, i.e.
> __bdos is passed as an additional size argument to a checking function before
> the actual libc function is called. Essentially, the __bdos is supposed to
> protect access to PTR and instead, ends up adding another invalid access to
> PTR. Admittedly, _FORTIFY_SOURCE is unable to protect pointer corruption,
> but at least it should not generate a new invalid dereference.
>
> To demonstrate how it could make things objectively worse, consider this
> example:
>
> struct fam_container
> {
> int a;
> int count;
> char fam[] __counted_by__ (count);
> };
>
> size_t
> baz (struct fam_container *ptr)
> {
> ...
> ptr->count = 0;
> __builtin_free (ptr);
> ...
> return 0;
> }
>
> void
> bar (size_t insz)
> {
> struct fam_container *c =
> __builtin_malloc (insz + sizeof (struct fam_container));
> c->count = insz;
> sz = baz (c);
> foo (c, sz);
> }
>
> void
> __attribute__ ((noinline))
> foo (struct fam_container *ptr, size_t sz)
> {
> ...
> objsz = __builtin_dynamic_object_size (ptr, 0);
> __memcpy_chk (ptr, src, sz, objsz);
> ...
> }
>
> Because SZ is 0, in practice there would have been no dereference of PTR in
> __memcpy_chk because the underlying memcpy would have not dereferenced PTR.
> Here, _FORTIFY_SOURCE ends up actually introducing an invalid dereference
> through the OBJSZ argument.
>
> I hope that clarifies what I'm trying to say.
Yes, Now I see what did you mean from the above example.
However, I think that the use-after-free error in the source code should be
caught by a separate tool -fsanitize=address. (And I just checked the above
small testing case with -fsanitize=address, Yes, the use-after-free error is
detected during run-time). It’s not the job of __builtin_dynamic_object_size
to catch such errors.
It should be reasonable to assume that in
__builtin_dynamic_object_size (ptr, 0);
ptr is NULL or point to a valid memory.
(I think that we might need clarify in the documentation of __bos and __bdos,
if an invalid pointer is passed to these builtins, the behavior is undefined)
Forcing compiler optimization or other sanitizers always check whether every
pointer reference might be use-after-free is not a reasonable requirement.
For example:
[opc@qinzhao-ol8u3-x86 ~]$ cat tt.c
struct fam_container
{
char fam[10];
int count;
};
void __attribute__ ((noinline))
baz (struct fam_container *ptr)
{
ptr->count = 0;
__builtin_free (ptr);
return;
}
void
__attribute__ ((noinline))
foo (struct fam_container *ptr)
{
__builtin_printf ("size of fam is %u \n”,
__builtin_dynamic_object_size (ptr->fam, 1));
}
void
bar ()
{
struct fam_container *c =
__builtin_malloc (sizeof (struct fam_container));
baz (c);
foo (c);
}
int main()
{
bar ();
return 0;
}
[opc@qinzhao-ol8u3-x86 ~]$ gcc -O2 tt.c;./a.out
size of fam is 10
In the above, we can see clearly that “ptr” is invalid when passed to
__builtin_dynamic_object_size,
However, the current __builtin_dynamic_object_size didn’t check such invalid
ptr and on the other hand, it
Incorrectly reports the size of the invalid pointer is 10. Is this wrong?
I think that for such behavior, we should clarify in the documentation of
__bdos and __bos that:
if an invalid pointer is passed to these builtins, __bdos and __bos does check
for this. the behavior is undefined.
This should resolve the issue.
WDYT?
Thanks.
Qing
>
> Sid