> On Jun 18, 2025, at 12:17, Siddhesh Poyarekar <siddh...@gotplt.org> 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

Reply via email to