Skip to content

[Clang] Add __builtin_counted_by_ref builtin #114495

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

Merged
merged 73 commits into from
Nov 7, 2024

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.
*_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?

@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.

@bwendling
Copy link
Collaborator Author

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

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

Fine, but I'm not touching anymore testcases again. I'm getting quite sick of you two ping-ponging back and forth on already good testcases.

@rapidsna rapidsna added the clang:bounds-safety Issue/PR relating to the experimental -fbounds-safety feature in Clang label Nov 7, 2024
@bwendling bwendling merged commit 7475156 into llvm:main Nov 7, 2024
7 of 9 checks passed
@nikic
Copy link
Contributor

nikic commented Nov 8, 2024

This change causes a large compile-time regression: https://llvm-compile-time-tracker.com/compare.php?from=ae9d0623ad65d84022bb4ed8446b6491451ae575&to=7475156d49406785a974b1205d11fe3de9c1553e&stat=instructions:u

From a quick look, I'd guess it's due the extra AST traversals you have added in SemaExpr.

@rapidsna
Copy link
Contributor

rapidsna commented Nov 8, 2024

@nikic thanks for reporting the issue. I filed #115520. We should probably be able to remove the AST traversals. I will take a look at it.

@bwendling
Copy link
Collaborator Author

I'll check it out. Sorry about the regression.

@bwendling bwendling deleted the builtin_get_counted_by branch November 12, 2024 00:41
Groverkss pushed a commit to iree-org/llvm-project that referenced this pull request Nov 15, 2024
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.

---------

Co-authored-by: Bill Wendling <[email protected]>
Co-authored-by: Aaron Ballman <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:bounds-safety Issue/PR relating to the experimental -fbounds-safety feature in Clang
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants