Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -8114,6 +8114,12 @@ def ext_gnu_ptr_func_arith : Extension<
"arithmetic on%select{ a|}0 pointer%select{|s}0 to%select{ the|}2 function "
"type%select{|s}2 %1%select{| and %3}2 is a GNU extension">,
InGroup<GNUPointerArith>;
def ext_gnu_counted_by_void_ptr
: Extension<
"'%select{counted_by|sized_by|counted_by_or_null|sized_by_or_null}0' "
"on a pointer to void is a GNU extension, treated as "
"'%select{sized_by|sized_by|sized_by_or_null|sized_by_or_null}0'">,
InGroup<GNUPointerArith>;
def err_readonly_message_assignment : Error<
"assigning to 'readonly' return result of an Objective-C message not allowed">;
def ext_c2y_increment_complex : Extension<
Expand Down
8 changes: 5 additions & 3 deletions clang/lib/CodeGen/CGBuiltin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1211,11 +1211,13 @@ llvm::Value *CodeGenFunction::emitCountedByPointerSize(
getContext().getTypeSizeInChars(ElementTy->getPointeeType());

if (ElementSize.isZero()) {
// This might be a __sized_by on a 'void *', which counts bytes, not
// elements.
// This might be a __sized_by (or __counted_by in GNU mode) on a
// 'void *', which counts bytes, not elements.
auto *CAT = ElementTy->getAs<CountAttributedType>();
if (!CAT || (CAT->getKind() != CountAttributedType::SizedBy &&
CAT->getKind() != CountAttributedType::SizedByOrNull))
CAT->getKind() != CountAttributedType::SizedByOrNull &&
CAT->getKind() != CountAttributedType::CountedBy &&
CAT->getKind() != CountAttributedType::CountedByOrNull))
// Okay, not sure what it is now.
// FIXME: Should this be an assert?
return std::optional<CharUnits>();
Expand Down
22 changes: 19 additions & 3 deletions clang/lib/Sema/SemaBoundsSafety.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,20 @@ bool Sema::CheckCountedByAttrOnField(FieldDecl *FD, Expr *E, bool CountInBytes,
// `BoundsSafetyCheckUseOfCountAttrPtr`
//
// * When the pointee type is always an incomplete type (e.g.
// `void`) the attribute is disallowed by this method because we know the
// type can never be completed so there's no reason to allow it.
InvalidTypeKind = CountedByInvalidPointeeTypeKind::INCOMPLETE;
// `void` in strict C mode) the attribute is disallowed by this method
// because we know the type can never be completed so there's no reason
// to allow it.
//
// Exception: In GNU mode, void has an implicit size of 1 byte for pointer
// arithmetic. Therefore, counted_by on void* is allowed as a GNU extension
// and behaves equivalently to sized_by (treating the count as bytes).
bool IsVoidPtrInGNUMode = PointeeTy->isVoidType() && getLangOpts().GNUMode;
Copy link
Contributor

@delcypher delcypher Oct 24, 2025

Choose a reason for hiding this comment

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

@rapidsna What do you think about changing this to

 bool IsVoidPtrInGNUMode = PointeeTy->isVoidType() && getLangOpts().GNUMode && !getLangOpts().BoundsSafety;

?

This would mean in the -fbounds-safety language model we would still disallow this. Or for consistency with -fno-bounds-safety do you think we should allow this language extension?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why does -fbounds-safety need this? I'd like to be bringing -fbounds-safety incrementally to Linux...

Copy link
Contributor

@rapidsna rapidsna Oct 24, 2025

Choose a reason for hiding this comment

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

I agree with @kees. From my perspective, allowing counted_by on void * without -fbounds-safety means we would have to allow it with -fbounds-safety as well (in GNU mode), since we want the same annotated headers to be consumable by the compiler with or without -fbounds-safety (except in some inevitable cases like inline function bodies in headers).

I think this is actually a reasonable compromise because treating void * as having a stride of 1 is a de facto standard in C.

However, in general, we cannot change the behavior of -fbounds-safety solely because the Linux community pushes back strongly. I think the issue is that they don't have the full context for -fbounds-safety, so decisions about individual attributes without that broader context can easily become opinionated (while this change isn't not unreasonable). I will be working to improve this situation.

Copy link
Contributor

Choose a reason for hiding this comment

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

That said, this is just my opinion. This change may lead to more of a radical shift than I initially thought because it affects the default behavior of -fbounds-safety in Clang (since GNU mode is on by default). So this is something I should discuss with our core -fbounds-safety engineers before making any decisions.

if (IsVoidPtrInGNUMode) {
// Emit a warning that this is a GNU extension
Diag(FD->getBeginLoc(), diag::ext_gnu_counted_by_void_ptr) << Kind;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should emit a note diagnostic here that suggests using __sized_by (or __sized_by_or_null) to remove this warning. Ideally we should add a fix-it to that too but that can be quite painful to implement because we don't have standardized way of writing the attributes (e.g. raw GNU attribute vs macro).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have it say what it is being interpreted as. What language would you prefer in here? I'm happy to swap it to whatever:

def ext_gnu_counted_by_void_ptr
    : Extension<
          "'%select{counted_by|sized_by|counted_by_or_null|sized_by_or_null}0' "
          "on a pointer to void is a GNU extension, treated as "
          "'%select{sized_by|sized_by|sized_by_or_null|sized_by_or_null}0'">,
      InGroup<GNUPointerArith>;

} else {
InvalidTypeKind = CountedByInvalidPointeeTypeKind::INCOMPLETE;
}
} else if (PointeeTy->isSizelessType()) {
InvalidTypeKind = CountedByInvalidPointeeTypeKind::SIZELESS;
} else if (PointeeTy->isFunctionType()) {
Expand Down Expand Up @@ -272,6 +283,11 @@ GetCountedByAttrOnIncompletePointee(QualType Ty, NamedDecl **ND) {
if (!PointeeTy->isIncompleteType(ND))
return {};

// If counted_by is on void*, it was already validated at declaration time
// as a GNU extension. No need to re-check GNU mode here.
if (PointeeTy->isVoidType())
return {};

return {CATy, PointeeTy};
}

Expand Down
65 changes: 65 additions & 0 deletions clang/test/CodeGen/attr-counted-by-void-ptr-gnu.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// RUN: %clang_cc1 -std=gnu11 -triple x86_64-unknown-linux-gnu -O2 -emit-llvm -o - %s | FileCheck %s

// Test that counted_by on void* in GNU mode treats void as having size 1 (byte count)

#define __counted_by(f) __attribute__((counted_by(f)))
#define __sized_by(f) __attribute__((sized_by(f)))

struct with_counted_by_void {
int count;
void* buf __counted_by(count);
};

struct with_sized_by_void {
int size;
void* buf __sized_by(size);
};

struct with_counted_by_int {
int count;
int* buf __counted_by(count);
};

// CHECK-LABEL: define dso_local {{.*}}@test_counted_by_void(
// CHECK: %[[COUNT:.*]] = load i32, ptr %s
// CHECK: %[[NARROW:.*]] = tail call i32 @llvm.smax.i32(i32 %[[COUNT]], i32 0)
// CHECK: %[[ZEXT:.*]] = zext nneg i32 %[[NARROW]] to i64
// CHECK: ret i64 %[[ZEXT]]
//
// Verify: counted_by on void* returns the count directly (count * 1 byte)
long long test_counted_by_void(struct with_counted_by_void *s) {
return __builtin_dynamic_object_size(s->buf, 0);
}

// CHECK-LABEL: define dso_local {{.*}}@test_sized_by_void(
// CHECK: %[[SIZE:.*]] = load i32, ptr %s
// CHECK: %[[NARROW:.*]] = tail call i32 @llvm.smax.i32(i32 %[[SIZE]], i32 0)
// CHECK: %[[ZEXT:.*]] = zext nneg i32 %[[NARROW]] to i64
// CHECK: ret i64 %[[ZEXT]]
//
// Verify: sized_by on void* returns the size directly
long long test_sized_by_void(struct with_sized_by_void *s) {
return __builtin_dynamic_object_size(s->buf, 0);
}

// CHECK-LABEL: define dso_local {{.*}}@test_counted_by_int(
// CHECK: %[[COUNT:.*]] = load i32, ptr %s
// CHECK: %[[SEXT:.*]] = sext i32 %[[COUNT]] to i64
// CHECK: %[[SIZE:.*]] = shl nsw i64 %[[SEXT]], 2
// CHECK: ret i64
//
// Verify: counted_by on int* returns count * sizeof(int) = count * 4
long long test_counted_by_int(struct with_counted_by_int *s) {
return __builtin_dynamic_object_size(s->buf, 0);
}

// CHECK-LABEL: define dso_local ptr @test_void_ptr_arithmetic(
// CHECK: %[[BUF:.*]] = load ptr, ptr
// CHECK: %[[EXT:.*]] = sext i32 %offset to i64
// CHECK: %[[PTR:.*]] = getelementptr inbounds i8, ptr %[[BUF]], i64 %[[EXT]]
// CHECK: ret ptr %[[PTR]]
//
// Verify: pointer arithmetic on void* uses i8 (byte offsets), not i32 or other sizes
void* test_void_ptr_arithmetic(struct with_counted_by_void *s, int offset) {
return s->buf + offset; // GNU extension: void* arithmetic
}
2 changes: 1 addition & 1 deletion clang/test/Sema/attr-counted-by-late-parsed-struct-ptrs.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
// RUN: %clang_cc1 -std=c11 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
Copy link
Member

Choose a reason for hiding this comment

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

What happens if -std=c11 is left unspecified?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Clang defaults to GNU mode AFAICT, so the negative tests fail.


#define __counted_by(f) __attribute__((counted_by(f)))

Expand Down
4 changes: 2 additions & 2 deletions clang/test/Sema/attr-counted-by-or-null-last-field.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// RUN: %clang_cc1 -fsyntax-only -verify=expected,immediate %s
// RUN: %clang_cc1 -fsyntax-only -fexperimental-late-parse-attributes -verify=expected,late %s
// RUN: %clang_cc1 -std=c11 -fsyntax-only -verify=expected,immediate %s
// RUN: %clang_cc1 -std=c11 -fsyntax-only -fexperimental-late-parse-attributes -verify=expected,late %s

#define __counted_by_or_null(f) __attribute__((counted_by_or_null(f)))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
// RUN: %clang_cc1 -std=c11 -fexperimental-late-parse-attributes -fsyntax-only -verify %s

#define __counted_by_or_null(f) __attribute__((counted_by_or_null(f)))
#define __counted_by(f) __attribute__((counted_by(f)))
Expand Down
4 changes: 2 additions & 2 deletions clang/test/Sema/attr-counted-by-or-null-struct-ptrs.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// RUN: %clang_cc1 -fsyntax-only -verify %s
// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify %s
// RUN: %clang_cc1 -std=c11 -fsyntax-only -verify %s
// RUN: %clang_cc1 -std=c11 -fexperimental-late-parse-attributes -fsyntax-only -verify %s

#define __counted_by_or_null(f) __attribute__((counted_by_or_null(f)))
#define __counted_by(f) __attribute__((counted_by(f)))
Expand Down
4 changes: 2 additions & 2 deletions clang/test/Sema/attr-counted-by-struct-ptrs.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// RUN: %clang_cc1 -fsyntax-only -verify %s
// RUN: %clang_cc1 -fsyntax-only -fexperimental-late-parse-attributes %s -verify
// RUN: %clang_cc1 -std=c11 -fsyntax-only -verify %s
// RUN: %clang_cc1 -std=c11 -fsyntax-only -fexperimental-late-parse-attributes %s -verify

#define __counted_by(f) __attribute__((counted_by(f)))

Expand Down
65 changes: 65 additions & 0 deletions clang/test/Sema/attr-counted-by-void-ptr-gnu.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// RUN: %clang_cc1 -std=gnu11 -fsyntax-only -verify=expected-nowarn %s
// RUN: %clang_cc1 -std=gnu11 -Wpointer-arith -fsyntax-only -verify=expected-warn %s
// RUN: %clang_cc1 -std=c11 -fsyntax-only -verify=expected-strict %s

// expected-nowarn-no-diagnostics

#define __counted_by(f) __attribute__((counted_by(f)))
#define __counted_by_or_null(f) __attribute__((counted_by_or_null(f)))
#define __sized_by(f) __attribute__((sized_by(f)))

//==============================================================================
// Test: counted_by on void* is allowed in GNU mode, rejected in strict mode
//==============================================================================

struct test_void_ptr_gnu {
int count;
// expected-warn-warning@+2{{'counted_by' on a pointer to void is a GNU extension, treated as 'sized_by'}}
// expected-strict-error@+1{{'counted_by' cannot be applied to a pointer with pointee of unknown size because 'void' is an incomplete type}}
void* buf __counted_by(count);
};

struct test_const_void_ptr_gnu {
int count;
// expected-warn-warning@+2{{'counted_by' on a pointer to void is a GNU extension, treated as 'sized_by'}}
// expected-strict-error@+1{{'counted_by' cannot be applied to a pointer with pointee of unknown size because 'const void' is an incomplete type}}
const void* buf __counted_by(count);
};

struct test_volatile_void_ptr_gnu {
int count;
// expected-warn-warning@+2{{'counted_by' on a pointer to void is a GNU extension, treated as 'sized_by'}}
// expected-strict-error@+1{{'counted_by' cannot be applied to a pointer with pointee of unknown size because 'volatile void' is an incomplete type}}
volatile void* buf __counted_by(count);
};

struct test_const_volatile_void_ptr_gnu {
int count;
// expected-warn-warning@+2{{'counted_by' on a pointer to void is a GNU extension, treated as 'sized_by'}}
// expected-strict-error@+1{{'counted_by' cannot be applied to a pointer with pointee of unknown size because 'const volatile void' is an incomplete type}}
const volatile void* buf __counted_by(count);
};

// Verify sized_by still works the same way (always allowed, no warning)
struct test_sized_by_void_ptr {
int size;
void* buf __sized_by(size); // OK in both modes, no warning
};

//==============================================================================
// Test: counted_by_or_null on void* behaves the same
//==============================================================================

struct test_void_ptr_or_null_gnu {
int count;
// expected-warn-warning@+2{{'counted_by_or_null' on a pointer to void is a GNU extension, treated as 'sized_by_or_null'}}
// expected-strict-error@+1{{'counted_by_or_null' cannot be applied to a pointer with pointee of unknown size because 'void' is an incomplete type}}
void* buf __counted_by_or_null(count);
};

struct test_const_void_ptr_or_null_gnu {
int count;
// expected-warn-warning@+2{{'counted_by_or_null' on a pointer to void is a GNU extension, treated as 'sized_by_or_null'}}
// expected-strict-error@+1{{'counted_by_or_null' cannot be applied to a pointer with pointee of unknown size because 'const void' is an incomplete type}}
const void* buf __counted_by_or_null(count);
};