About where we should insert the new __builtin_with_access_and_size:

> On Oct 26, 2023, at 2:54 PM, Qing Zhao <qing.z...@oracle.com> wrote:
> 
> 
> 
>> On Oct 26, 2023, at 10:05 AM, Richard Biener <richard.guent...@gmail.com> 
>> wrote:
>> 
>> 
>> 
>>> Am 26.10.2023 um 12:14 schrieb Martin Uecker <uec...@tugraz.at>:
>>> 
>>> Am Donnerstag, dem 26.10.2023 um 11:20 +0200 schrieb Martin Uecker:
>>>>> Am Donnerstag, dem 26.10.2023 um 10:45 +0200 schrieb Richard Biener:
>>>>> On Wed, Oct 25, 2023 at 8:16 PM Martin Uecker <uec...@tugraz.at> wrote:
>>>>>> 
>>>>>> Am Mittwoch, dem 25.10.2023 um 13:13 +0200 schrieb Richard Biener:
>>>>>>> 
>>>>>>>> Am 25.10.2023 um 12:47 schrieb Martin Uecker <uec...@tugraz.at>:
>>>>>>>> 
>>>>>>>> Am Mittwoch, dem 25.10.2023 um 06:25 -0400 schrieb Siddhesh Poyarekar:
>>>>>>>>>> On 2023-10-25 04:16, Martin Uecker wrote:
>>>>>>>>>> Am Mittwoch, dem 25.10.2023 um 08:43 +0200 schrieb Richard Biener:
>>>>>>>>>>> 
>>>>>>>>>>>> Am 24.10.2023 um 22:38 schrieb Martin Uecker <uec...@tugraz.at>:
>>>>>>>>>>>> 
>>>>>>>>>>>> Am Dienstag, dem 24.10.2023 um 20:30 +0000 schrieb Qing Zhao:
>>>>>>>>>>>>> Hi, Sid,
>>>>>>>>>>>>> 
>>>>>>>>>>>>> Really appreciate for your example and detailed explanation. Very 
>>>>>>>>>>>>> helpful.
>>>>>>>>>>>>> I think that this example is an excellent example to show 
>>>>>>>>>>>>> (almost) all the issues we need to consider.
>>>>>>>>>>>>> 
>>>>>>>>>>>>> I slightly modified this example to make it to be compilable and 
>>>>>>>>>>>>> run-able, as following:
>>>>>>>>>>>>> (but I still cannot make the incorrect reordering or DSE 
>>>>>>>>>>>>> happening, anyway, the potential reordering possibility is there…)
>>>>>>>>>>>>> 
>>>>>>>>>>>>> 1 #include <malloc.h>
>>>>>>>>>>>>> 2 struct A
>>>>>>>>>>>>> 3 {
>>>>>>>>>>>>> 4  size_t size;
>>>>>>>>>>>>> 5  char buf[] __attribute__((counted_by(size)));
>>>>>>>>>>>>> 6 };
>>>>>>>>>>>>> 7
>>>>>>>>>>>>> 8 static size_t
>>>>>>>>>>>>> 9 get_size_from (void *ptr)
>>>>>>>>>>>>> 10 {
>>>>>>>>>>>>> 11  return __builtin_dynamic_object_size (ptr, 1);
>>>>>>>>>>>>> 12 }
>>>>>>>>>>>>> 13
>>>>>>>>>>>>> 14 void
>>>>>>>>>>>>> 15 foo (size_t sz)
>>>>>>>>>>>>> 16 {
>>>>>>>>>>>>> 17  struct A *obj = __builtin_malloc (sizeof(struct A) + sz * 
>>>>>>>>>>>>> sizeof(char));
>>>>>>>>>>>>> 18  obj->size = sz;
>>>>>>>>>>>>> 19  obj->buf[0] = 2;
>>>>>>>>>>>>> 20  __builtin_printf (“%d\n", get_size_from (obj->buf));
>>>>>>>>>>>>> 21  return;
>>>>>>>>>>>>> 22 }
>>>>>>>>>>>>> 23
>>>>>>>>>>>>> 24 int main ()
>>>>>>>>>>>>> 25 {
>>>>>>>>>>>>> 26  foo (20);
>>>>>>>>>>>>> 27  return 0;
>>>>>>>>>>>>> 28 }
>>>>>>>>>>>>> 
>>>>>>>>> 
>>>>>>>>> <snip>
>>>>>>>>> 
>>>>>>>>>>> When it’s set I suppose.  Turn
>>>>>>>>>>> 
>>>>>>>>>>> X.l = n;
>>>>>>>>>>> 
>>>>>>>>>>> Into
>>>>>>>>>>> 
>>>>>>>>>>> X.l = __builtin_with_size (x.buf, n);
>>>>>>>>>> 
>>>>>>>>>> It would turn
>>>>>>>>>> 
>>>>>>>>>> some_variable = (&) x.buf
>>>>>>>>>> 
>>>>>>>>>> into
>>>>>>>>>> 
>>>>>>>>>> some_variable = __builtin_with_size ( (&) x.buf. x.len)
>>>>>>>>>> 
>>>>>>>>>> 
>>>>>>>>>> So the later access to x.buf and not the initialization
>>>>>>>>>> of a member of the struct (which is too early).
>>>>>>>>>> 
>>>>>>>>> 
>>>>>>>>> Hmm, so with Qing's example above, are you suggesting the 
>>>>>>>>> transformation
>>>>>>>>> be to foo like so:
>>>>>>>>> 
>>>>>>>>> 14 void
>>>>>>>>> 15 foo (size_t sz)
>>>>>>>>> 16 {
>>>>>>>>> 16.5  void * _1;
>>>>>>>>> 17  struct A *obj = __builtin_malloc (sizeof(struct A) + sz * 
>>>>>>>>> sizeof(char));
>>>>>>>>> 18  obj->size = sz;
>>>>>>>>> 19  obj->buf[0] = 2;
>>>>>>>>> 19.5  _1 = __builtin_with_size (obj->buf, obj->size);
>>>>>>>>> 20  __builtin_printf (“%d\n", get_size_from (_1));
>>>>>>>>> 21  return;
>>>>>>>>> 22 }
>>>>>>>>> 
>>>>>>>>> If yes then this could indeed work.  I think I got thrown off by the
>>>>>>>>> reference to __bdos.
>>>>>>>> 
>>>>>>>> Yes. I think it is important not to evaluate the size at the
>>>>>>>> access to buf and not the allocation, because the point is to
>>>>>>>> recover it from the size member even when the compiler can't
>>>>>>>> see the original allocation.
>>>>>>> 
>>>>>>> But if the access is through a pointer without the attribute visible
>>>>>>> even the Frontend cannot recover?
>>>>>> 
>>>>>> Yes, if the access is using a struct-with-FAM without the attribute
>>>>>> the FE would not be insert the builtin.  BDOS could potentially
>>>>>> still see the original allocation but if it doesn't, then there is
>>>>>> no information.
>>>>>> 
>>>>>>> We’d need to force type correctness and give up on indirecting
>>>>>>> through an int * when it can refer to two diffenent container types.
>>>>>>> The best we can do I think is mark allocation sites and hope for
>>>>>>> some basic code hygiene (not clobbering size or array pointer
>>>>>>> through pointers without the appropriately attributed type)
>>>>>> 
>>>>>> I am do not fully understand what you are referring to.
>>>>> 
>>>>> struct A { int n; int data[n]; };
>>>>> struct B { long n; int data[n]; };
>>>>> 
>>>>> int *p = flag ? a->data : b->data;
>>>>> 
>>>>> access *p;
>>>>> 
>>>>> Since we need to allow interoperability of pointers (a->data is
>>>>> convertible to a non-fat pointer of type int *) this leaves us with
>>>>> ambiguity we need to conservatively handle to avoid false positives.
>>>> 
>>>> For BDOS, I would expect this to work exactly like:
>>>> 
>>>> char aa[n1];
>>>> char bb[n2];
>>>> char *p = flag ? aa : bb;
>>>> 
>>>> (or similar code with malloc). In fact it does:
>>>> 
>>>> https://godbolt.org/z/bK68YKqhe
>>>> (cheating a bit and also the sub-object version of
>>>> BDOS does not seem to work)
>>>> 
>>>>> 
>>>>> We _might_ want to diagnose decay of a->data to int *, but IIRC
>>>>> there's no way (or proposal) to allow declaring a corresponding
>>>>> fat pointer, so it's not a good designed feature.
>>>> 
>>>> As a language feature, I fully agree.  I see the
>>>> counted_by attribute has a makeshift solution.
>>>> 
>>>> But we can already do:
>>>> 
>>>> auto p = flag ? &aa : &bb;
>>>> 
>>>> and this already works perfectly:
>>>> 
>>>> https://godbolt.org/z/rvb6xWWPj
>>>> 
>>>> We can also name the variably-modifed type: 
>>>> 
>>>> char (*p)[flag ? n1 : n2] = flag ? &aa : &bb;
>>>> https://godbolt.org/z/13cTT1vGP
>>>> 
>>>> The problem with this version is that consistency
>>>> is not checked. (I have patch for adding run-time
>>>> checks).
>>>> 
>>>> And then the next step would be to allow
>>>> 
>>>> char (*p)[:] = flag ? &aa : &bb;
>>>> 
>>>> or similar.  Dennis Ritchie proposed this himself
>>>> a long time ago.
>>>> 
>>>> So far this seems straightfoward.
>>>> 
>>>> If we then want to allow such wide pointers as
>>>> function arguments or in structs, we would need
>>>> to define an ABI. But the ABI could just be
>>>> 
>>>> struct { char (*p)[.s]; size_t s; };
>>>> 
>>>> Maybe we could try to make the following
>>>> ABI compatible:
>>>> 
>>>> int foo(int p[s], size_t s);
>>>> int foo(int p[:]);
>>>> 
>>>> 
>>>>> Having __builtin_with_size at allocation would possibly make
>>>>> the BOS use-def walk discover both objects.
>>>> 
>>>> Yes. But I do not think this there is any fundamental
>>>> difference to discovering allocation functions.
>>>> 
>>>>> I think you can't
>>>>> insert __builtin_with_size at the access to *p, but in practice
>>>>> that would be very much needed.
>>>> 
>>>> Usually the access to *p would follow directly the
>>>> access x.buf, so BDOS should find it.
>>>> 
>>>> But yes, to get full bounds safety, the pointer type 
>>>> has to change to a variably-modified type (which would work
>>>> today) or a fat pointer type. The later can be built on
>>>> vm-types easily because all the FE semantics already
>>>> exists.
>>> 
>>> We could insert the __builtin_with_size everywhere
>>> we have to convert a wide pointer or let an array
>>> decay to traditional pointer for reason of compatibility 
>>> with legacy code.
>> 
>> That sounds like a nice idea.  Note I’d like to see the consumer side 
>> implemented so we can play with different points of insertion (and I’ll try 
>> to show corner cases where it goes wrong).
> 
> Giving the example I mentioned previously:
> 
>  1 #include <malloc.h>
>  2 struct A
>  3 {
>  4  size_t size;
>  5  char buf[] __attribute__((counted_by(size)));
>  6 };
>  7 
>  8 static size_t
>  9 get_size_from (void *ptr)
> 10 {
> 11  return __builtin_dynamic_object_size (ptr, 1);
> 12 }
> 13 
> 14 void
> 15 foo (size_t sz)
> 16 {
> 17  struct A *obj = __builtin_malloc (sizeof(struct A) + sz * sizeof(char));
> 18  obj->size = sz;
> 19  obj->buf[0] = 2;
> 20  __builtin_printf (“%d\n", get_size_from (obj->buf));
> 21  return;
> 22 }
> 
> So, the different points of insertion the new __builtin_with_size in FE 
> include the following points: (per my understanding so far)
> 
> Point 1. When the “obj->buf” is referenced at line 19, and line 20?
> Point 2. When the “obj” is allocated at line 17? 
> 
> Are these correct?
> 
> Any other points we need to consider?

After more thinking, I think for the current “counted_by” attribute, the best 
insertion point is 

Point 1: when the “obj->buf” is referenced.

As we discussed before, we need to add a semantic requirement in the user 
documentation first:

The setting to “obj->size” should be done before the first reference to 
“obj->buf”. 

So, only when the “obj->buf” is referenced, we can guarantee that the 
“obj->size” is initialized already. 
No additional check for whether “obj->size” is valid in the IL when adding the 
__builtin_with_access_and_size.

> 
> 
>> It all seems a bit late for GCC 14 though.
> 
> Yes, I agree.  Given the potential impact on the code generation and other 
> potential issues, it’s better to be put in the early stage of the next 
> release.

I will write a detailed proposal based on the discussion so far with the 
following information:
   1. User interface for counted_by attribute and user level requirement;
   2. __builtin_with_access_and_size() approach for implementation:
       2.1 definition of this new internal fn;
       2.2 implementation details in FE and Middle end;
   3. Testings;

And send out for more comments.

Let me know if you have more suggestions.

Thanks a lot for all the help.

Qing
> 
> Qing
>> 
>> Richard 
>> 
>>> Martin
>>> 
>>>> 
>>>> Martin
>>>> 
>>>>> 
>>>>> Richard.
>>>>> 
>>>>>> But yes,
>>>>>> for full bounds safety we would need the language feature.
>>>>>> In C people should start to variably-modified types
>>>>>> more.  I think we can build perfect bounds safety on top of
>>>>>> them in a very good way with only FE changes.
>>>>>> 
>>>>>> All these attributes are just a best effort.  But for a while,
>>>>>> this will be necessary.
>>>>>> 
>>>>>> Martin
>>>>>> 
>>>>>>> 
>>>>>>>> Evaluating at this point requires that the size is correctly set
>>>>>>>> before the access to the FAM and the user has to make sure
>>>>>>>> this is the case. But to me this requirement would make sense.
>>>>>>>> 
>>>>>>>> Semantically, it could aöso make sense to evaluate the size at a
>>>>>>>> later time.  But then the reordering becomes problematic again.
>>>>>>>> 
>>>>>>>> Also I think this would make this feature generally more useful.
>>>>>>>> For example, it could work also for others pointers in the struct
>>>>>>>> and not just for FAMs.  In this case, the struct may already be
>>>>>>>> freed when  BDOS is called, so it might also not possible to
>>>>>>>> access the size member at a later time.
>>>>>>>> 
>>>>>>>> Martin
> 

Reply via email to