Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clang] Add __builtin_counted_by_ref builtin #114495

Open
wants to merge 68 commits into
base: main
Choose a base branch
from

Conversation

bwendling
Copy link
Collaborator

The __builtin_counted_by_ref builtin is used on a flexible array
pointer and returns a pointer to the "counted_by" attribute's COUNT
argument, which is a field in the same non-anonymous struct as the
flexible array member. This is useful for automatically setting the
count field without needing the programmer's intervention. Otherwise
it's possible to get this anti-pattern:

  ptr = alloc(<ty>, ..., COUNT);
  ptr->FAM[9] = 42; /* <<< Sanitizer will complain */
  ptr->count = COUNT;

To prevent this anti-pattern, the user can create an allocator that
automatically performs the assignment:

  #define alloc(TY, FAM, COUNT) ({ \
      TY __p = alloc(get_size(TY, COUNT));             \
      if (__builtin_counted_by_ref(__p->FAM))          \
          *__builtin_counted_by_ref(__p->FAM) = COUNT; \
      __p;                                             \
  })

The builtin's behavior is heavily dependent upon the "counted_by"
attribute existing. It's main utility is during allocation to avoid
the above anti-pattern. If the flexible array member doesn't have that
attribute, the builtin becomes a no-op. Therefore, if the flexible
array member has a "count" field not referenced by "counted_by", it
must be set explicitly after the allocation as this builtin will
return a "nullptr" and the assignment will most likely be elided.

bwendling and others added 30 commits October 31, 2024 18:02
The __builtin_counted_by_ref builtin is used on a flexible array
pointer and returns a pointer to the "counted_by" attribute's COUNT
argument, which is a field in the same non-anonymous struct as the
flexible array member. This is useful for automatically setting the
count field without needing the programmer's intervention. Otherwise
it's possible to get this anti-pattern:

  ptr = alloc(<ty>, ..., COUNT);
  ptr->FAM[9] = 42; /* <<< Sanitizer will complain */
  ptr->count = COUNT;

To prevent this anti-pattern, the user can create an allocator that
automatically performs the assignment:

  #define alloc(TY, FAM, COUNT) ({ \
      TY __p = alloc(get_size(TY, COUNT));             \
      if (__builtin_counted_by_ref(__p->FAM))          \
          *__builtin_counted_by_ref(__p->FAM) = COUNT; \
      __p;                                             \
  })
The builtin's behavior is heavily dependent upon the "counted_by"
attribute existing. It's main utility is during allocation to avoid
the above anti-pattern. If the flexible array member doesn't have that
attribute, the builtin becomes a no-op. Therefore, if the flexible
array member has a "count" field not referenced by "counted_by", it
must be set explicitly after the allocation as this builtin will
return a "nullptr" and the assignment will most likely be elided.
…d ignores pretty much everythings that may 'hide' the underlying MemberExpr.
…t the return type based on the 'count' field's type.
FindCountedByField can be used in more places than CodeGen. Move it into
FieldDecl to avoid layering issues.
…. Go back to 'size_t *' as the default return type.
@bwendling
Copy link
Collaborator Author

@AaronBallman @rapidsna -- I think I covered all of the comments you made on the other PR (stupid git!). PTAL.

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test with -ast-print that shows we print out the builtin as it's used in the source, and a test with -ast-dump that shows the AST retains information about the use of the builtin?

@bwendling
Copy link
Collaborator Author

-ast-print testcase added.

*_Generic(
__builtin_counted_by_ref(&p->array[0]),
void *: &__ignored,
default: __builtin_counted_by_ref(&p->array[idx++])) = size;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you have a test with a side effect. Though idx++ is a parameter so the side effect would be optimized out in this O2 test anyway even if it was codegen'ed. Could you please use something that won't be optimized out to make sure the test is really checking it's not codegen'ed?

Also, is it intentionally to make &p->array[non-zero] work?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, why are we doing -O2 in a test to begin with? Afaik codegen tests are usually supposed to run w/o optimisation; otherwise, you just end up testing the optimiser, and that’s not what Clang’s regression tests are for, after all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the -O2 now. So the idx shouldn't be optimized away.

Copy link
Contributor

@rapidsna rapidsna Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Do you have a test when the base of member has a side effect? e.g., __builtin_counted_by_ref(foo().array) The behavior seems to be different than idx++ case, because when the base has a side effect the codegen seems to return (void *)0 instead of returning the count reference. My inclination is to reject code or emitting a different warning message when the base has a side effect because the programmer might want to fix that instead of it being silently ignored or confused about the behavior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do something like this:

      *_Generic(
          __flex_counter((p++)->array),
              void *: &__ignored_assignment,
              default: __flex_counter(p->array)) = 42;

The code generation seems to take it in stride (with a warning of course):

define dso_local noalias noundef ptr @alloc_s(i32 noundef %size) local_unnamed_addr #0 {
entry:
  %0 = load ptr, ptr @p, align 8, !tbaa !5
  %..counted_by.gep = getelementptr inbounds i8, ptr %0, i64 4
  store i8 42, ptr %..counted_by.gep, align 1, !tbaa !9
  ret ptr null
}

I'm not sure if that's good or bad. In truth, the warning should probably be upgraded to an error, as it will lead to unexpected behavior if the warning is ignored. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, upgrading to an error sounds good to me! Also, the message might need to be updated (since the side effect is not actually ignored).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -16,6 +16,8 @@ void test1(struct fam_struct *ptr, int size, int idx) {

*__builtin_counted_by_ref(ptr->array) = size; // ok
*__builtin_counted_by_ref(&ptr->array[idx]) = size; // ok
*__builtin_counted_by_ref(&ptr->array) = size; // ok
Copy link
Contributor

@rapidsna rapidsna Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay? Or should we make it an ill-form?

Copy link
Collaborator Author

@bwendling bwendling Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it generates correct code:

  {
      unsigned long int __ignored_assignment;
      *_Generic(
          __flex_counter(&p->array),
              void *: &__ignored_assignment,
              default: __flex_counter(&p->array)) = 42;
  }

/*
define dso_local noalias noundef ptr @alloc_s(i32 noundef %size) local_unnamed_addr #0 {
entry:
  %0 = load ptr, ptr @p, align 8, !tbaa !5
  %..counted_by.gep = getelementptr inbounds i8, ptr %0, i64 4
  store i8 42, ptr %..counted_by.gep, align 1, !tbaa !9
  ret ptr null
}
 */

But if you prefer it to be ill-formed, then I can do that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this will just work too? *__builtin_counted_by_ref(ptr->array[idx]) = size;

Do you have a use case? I'd prefer these being ill-formed because the user might find it working unexpectedly. But I don't have a strong opinion on this. I'd leave it up to you/Linux users if you find this would be useful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*__builtin_counted_by_ref(ptr->array[idx]) = size; doesn't work because the builtin requires a pointer to the FAM. (If you meant &ptr->array[idx], that does work.)

I don't have any use case for &ptr->array or the like. I just don't want to be overly restrictive on what's considered a valid argument to the builtin. I think I'll do a comparison with gcc's version and see what they accept. We can match them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GCC rejects everything that isn't an array. So not even &ptr->array[0]. I guess we can do that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that, because GCC only checks if the argument is an array, it allows for things like:

int global_array[];

void foo(int val) {
  *__builtin_counted_by_ref(global_array) = val;
}

which isn't great.

Copy link
Contributor

@rapidsna rapidsna Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can still keep it an error and have GCC to reflect our model on this one, unless Linux relies on this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I don't want to allow for global arrays either, just flexible array members.

@bwendling
Copy link
Collaborator Author

Okay. I removed the -O2 from the testcases. That should prevent optimizations from removing the idx++. I removed the frivolous setting if the InBounds. As for the &ptr->array question, if we're able to get the proper information from it I don't see why we can't have it :-). Anything else?

void test3(struct fam_struct *ptr, int idx) {
__builtin_counted_by_ref(&ptr->array[idx]); // expected-error {{'__builtin_counted_by_ref' argument must reference a flexible array member}}
__builtin_counted_by_ref(&ptr->array); // expected-error {{'__builtin_counted_by_ref' argument must reference a flexible array member}}
__builtin_counted_by_ref(ptr->x); // expected-error {{'__builtin_counted_by_ref' argument must reference a flexible array member}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we can also add __builtin_counted_by_ref(ptr->array[0]);

@rapidsna
Copy link
Contributor

rapidsna commented Nov 5, 2024

I still see CodeGen/attr-counted-by*.c being unnecessarily changed. Should we just revert it back to llvm:main. Other than a couple of minor comments on the tests, LGTM! Thanks for addressing feedback. You might still wait on @AaronBallman though.

@Sirraide
Copy link
Member

Sirraide commented Nov 6, 2024

I still see CodeGen/attr-counted-by*.c being unnecessarily changed.

Yeah, the -O2 definitely needs to be added back to those tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants