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
2 changes: 1 addition & 1 deletion clang/include/clang/Basic/Builtins.td
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ def BuiltinAssumeAligned : Builtin {

def BuiltinAssumeDereferenceable : Builtin {
let Spellings = ["__builtin_assume_dereferenceable"];
let Attributes = [NoThrow, Const];
let Attributes = [NoThrow, Const, Constexpr];
let Prototype = "void(void const*, size_t)";
}

Expand Down
52 changes: 52 additions & 0 deletions clang/lib/AST/ByteCode/InterpBuiltin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2170,6 +2170,55 @@ static unsigned computePointerOffset(const ASTContext &ASTCtx,
return Result;
}

/// __builtin_assume_dereferenceable(Ptr, Size)
static bool interp__builtin_assume_dereferenceable(InterpState &S, CodePtr OpPC,
const InterpFrame *Frame,
const CallExpr *Call) {
assert(Call->getNumArgs() == 2);

APSInt ReqSize = popToAPSInt(S, Call->getArg(1));
const Pointer &Ptr = S.Stk.pop<Pointer>();

if (ReqSize.isZero())
return true;
if (Ptr.isZero()) {
S.FFDiag(S.Current->getSource(OpPC), diag::note_constexpr_access_null)
<< AK_Read << S.Current->getRange(OpPC);
return false;
}
if (!Ptr.isBlockPointer()) {
if (Ptr.isIntegralPointer())
return true;
return false;
}
if (!Ptr.isLive())
return false;
if (Ptr.isPastEnd()) {
S.FFDiag(S.Current->getSource(OpPC), diag::note_constexpr_access_past_end)
<< AK_Read << S.Current->getRange(OpPC);
return false;
}

const ASTContext &ASTCtx = S.getASTContext();
const Descriptor *DeclDesc = Ptr.getDeclDesc();
std::optional<unsigned> FullSize = computeFullDescSize(ASTCtx, DeclDesc);
if (!FullSize)
return false;

unsigned ByteOffset = computePointerOffset(ASTCtx, Ptr);
if (ByteOffset > *FullSize)
return false;

unsigned AvailSize = *FullSize - ByteOffset;
if (AvailSize < ReqSize.getZExtValue()) {
S.FFDiag(S.Current->getSource(OpPC), diag::note_constexpr_access_past_end)
<< AK_Read << S.Current->getRange(OpPC);
return false;
}

return true;
}

/// Does Ptr point to the last subobject?
static bool pointsToLastObject(const Pointer &Ptr) {
Pointer P = Ptr;
Expand Down Expand Up @@ -3725,6 +3774,9 @@ bool InterpretBuiltin(InterpState &S, CodePtr OpPC, const CallExpr *Call,
case Builtin::BI__assume:
return interp__builtin_assume(S, OpPC, Frame, Call);

case Builtin::BI__builtin_assume_dereferenceable:
return interp__builtin_assume_dereferenceable(S, OpPC, Frame, Call);

case Builtin::BI__builtin_strcmp:
case Builtin::BIstrcmp:
case Builtin::BI__builtin_strncmp:
Expand Down
40 changes: 40 additions & 0 deletions clang/lib/AST/ExprConstant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19690,6 +19690,46 @@ class VoidExprEvaluator
// The argument is not evaluated!
return true;

case Builtin::BI__builtin_assume_dereferenceable: {
assert(E->getType()->isVoidType());
assert(E->getNumArgs() == 2);

APSInt ReqSizeVal;
if (!::EvaluateInteger(E->getArg(1), ReqSizeVal, Info))
return false;
if (ReqSizeVal.isZero())
return true;

LValue Pointer;
if (!EvaluatePointer(E->getArg(0), Pointer, Info)) {
if (EvaluateBuiltinConstantP(Info, E->getArg(0)))
return true;
return false;
}
if (Pointer.Designator.Invalid)
return false;
if (Pointer.isNullPointer()) {
Info.FFDiag(E, diag::note_constexpr_access_null) << AK_Read;
return false;
}
if (Pointer.Designator.isOnePastTheEnd()) {
Info.FFDiag(E, diag::note_constexpr_access_past_end) << AK_Read;
return false;
}

uint64_t ReqSize = ReqSizeVal.getZExtValue();
CharUnits EndOffset;
if (!determineEndOffset(Info, E->getExprLoc(), 0, Pointer, EndOffset))
return false;

uint64_t AvailSize =
(EndOffset - Pointer.getLValueOffset()).getQuantity();
if (AvailSize < ReqSize) {
Info.FFDiag(E, diag::note_constexpr_access_past_end) << AK_Read;
return false;
}
return true;
}
case Builtin::BI__builtin_operator_delete:
return HandleOperatorDeleteCall(Info, E);

Expand Down
56 changes: 56 additions & 0 deletions clang/test/CodeGenCXX/builtin-assume-dereferenceable-constexpr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// RUN: %clang_cc1 -triple x86_64-unknown-unknown -std=c++14 -emit-llvm -o - %s | FileCheck %s
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing RUN line here.


int b = 10;
const int f = (__builtin_assume_dereferenceable((char*)&b + 1, 3), 12);
int use_f = f;

constexpr int g = 20;
const int h = (__builtin_assume_dereferenceable((char*)&g + 1, 2), 42);
int use_h = h;

constexpr char arr[] = "hello";
constexpr const char* ptr = arr + 1;
constexpr int fully_constexpr() {
__builtin_assume_dereferenceable(ptr, 2);
return 100;
}
constexpr int i = fully_constexpr();
int use_i = i;

void test_integral_ptr() {
__builtin_assume_dereferenceable((int*)0x1234, 4);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this example is incorrect, sorry. There are no readable bytes after that address.

}

void test_nullptr() {
__builtin_assume_dereferenceable(nullptr, 0);
}

void test_zero_size() {
int x = 10;
__builtin_assume_dereferenceable(&x, 0);
}

void test_function_ptr() {
__builtin_assume_dereferenceable((void*)&test_zero_size, 8);
}

// CHECK: @use_i = global i32 100
//
// CHECK: @{{_Z[0-9]+}}test_integral_ptrv
// CHECK: call void @llvm.assume(i1 true) [ "dereferenceable"(ptr inttoptr (i64 4660 to ptr), i64 4) ]
//
// CHECK: @{{_Z[0-9]+}}test_nullptrv
// CHECK: call void @llvm.assume(i1 true) [ "dereferenceable"(ptr null, i64 0) ]
//
// CHECK: @{{_Z[0-9]+}}test_zero_sizev
// CHECK: call void @llvm.assume(i1 true) [ "dereferenceable"(ptr {{%.*}}, i64 0) ]
//
// CHECK: @{{_Z[0-9]+}}test_function_ptrv
// CHECK: call void @llvm.assume(i1 true) [ "dereferenceable"(ptr @{{_Z[0-9]+}}test_zero_sizev, i64 8) ]
//
// CHECK: __cxx_global_var_init
// CHECK: call void @llvm.assume(i1 true) [ "dereferenceable"(ptr getelementptr inbounds (i8, ptr @b, i64 1), i64 3) ]
//
// CHECK: __cxx_global_var_init
// CHECK: call void @llvm.assume(i1 true) [ "dereferenceable"(ptr getelementptr inbounds (i8, ptr @{{_ZL[0-9]+}}g, i64 1), i64 2) ]
// CHECK: store i32 42, ptr @{{_ZL[0-9]+}}h
92 changes: 92 additions & 0 deletions clang/test/SemaCXX/builtin-assume-dereferenceable-constexpr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// RUN: %clang_cc1 -fsyntax-only -verify -std=c++14 -triple x86_64-unknown-unknown %s
// RUN: %clang_cc1 -fsyntax-only -verify -std=c++14 -triple x86_64-unknown-unknown %s -fexperimental-new-constant-interpreter

constexpr bool test_constexpr_valid() {
constexpr int arr[10] = {};
__builtin_assume_dereferenceable(arr, 40);
return true;
}
static_assert(test_constexpr_valid(), "");

constexpr bool test_constexpr_partial() {
constexpr int arr[10] = {};
__builtin_assume_dereferenceable(&arr[5], 20);
return true;
}
static_assert(test_constexpr_partial(), "");

constexpr bool test_constexpr_nullptr() { // expected-error {{constexpr function never produces a constant expression}}
__builtin_assume_dereferenceable(nullptr, 4); // expected-note 2{{read of dereferenced null pointer is not allowed in a constant expression}}
return true;
}
static_assert(test_constexpr_nullptr(), ""); // expected-error {{not an integral constant expression}} expected-note {{in call to}}

constexpr bool test_constexpr_too_large() { // expected-error {{constexpr function never produces a constant expression}}
constexpr int arr[10] = {};
__builtin_assume_dereferenceable(arr, 100); // expected-note 2{{read of dereferenced one-past-the-end pointer is not allowed in a constant expression}}
return true;
}
static_assert(test_constexpr_too_large(), ""); // expected-error {{not an integral constant expression}} expected-note {{in call to}}

constexpr bool test_single_var() {
constexpr int single_var = 42;
__builtin_assume_dereferenceable(&single_var, 4);
return true;
}
static_assert(test_single_var(), "");

constexpr bool test_exact_boundary() {
constexpr int arr[10] = {};
__builtin_assume_dereferenceable(&arr[9], 4);
return true;
}
static_assert(test_exact_boundary(), "");

constexpr bool test_one_over() { // expected-error {{constexpr function never produces a constant expression}}
constexpr int arr[10] = {};
__builtin_assume_dereferenceable(&arr[9], 5); // expected-note 2{{read of dereferenced one-past-the-end pointer is not allowed in a constant expression}}
return true;
}
static_assert(test_one_over(), ""); // expected-error {{not an integral constant expression}} expected-note {{in call to}}

constexpr bool test_zero_size() {
constexpr int arr[10] = {};
__builtin_assume_dereferenceable(arr, 0);
return true;
}
static_assert(test_zero_size(), "");

constexpr bool test_struct_member() {
struct S {
int x;
int y;
};
constexpr S s = {1, 2};
__builtin_assume_dereferenceable(&s.x, 4);
return true;
}
static_assert(test_struct_member(), "");
Copy link
Contributor

@tbaederr tbaederr Nov 28, 2025

Choose a reason for hiding this comment

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

You're only testing constant contexts.
For e.g.

const int f = (__builtin_assume_dereferenceable((int*)123, 4), 12);
int a = f;

should we get a global initializer? I can read 4 bytes from that pointer, right?

I'm not 100% sure how this builtin is supposed to work in any case. I initially thought it would return the status but if it doesn't, does it make sense to just ignore it in constant expressions? All actual reads will be checked as usual. @philnik777?

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be a valid implementation to just ignore the builtin I guess, but I'd be much happier if it actually checked that bytes are dereferenceable. That's quite useful to catch problems in loops that can return early. e.g. find_if requires the whole range to be dereferenceable, but may never actually reach the end of the data. The compiler is still allowed to read past the element that is returned, resulting potentially in crashes.

In general my goal is to catch all the UB possible during constant evaluation that the compiler can optimize on. With __builtin_assume_dereferenceable we currently can produce a program which, with identical inputs, is accepted during constant evaluation by the compiler but crashes during runtime. I'm aware of some other places this is the case, but I'd rather reduce that number, not increase it.

I'm not quite sure what you're asking in your example. It's not a constant expression at this point, is it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, I thought the builtin meant "we can dereference that pointer and read X bytes from it", not "starting from that pointer, X bytes can be read" (the latter meaning we can basically to a memcpy from it).

What about

int b = 10;
const int f = (__builtin_assume_dereferenceable((char*)&b + 1, 3), 12);
int a = f;

, shouldn't that work?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that should work if you were able to inspect the byte representation of objects during constant evaluation. It's definitely a correct use of the attribute though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the example is not a valid constexpr

<source>:3:16: note: subexpression not valid in a constant expression
    3 | const int f = (__builtin_assume_dereferenceable((char*)&b + 1, 3), 12);
      |                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:10:20: error: constexpr variable 'f' must be initialized by a constant expression
   10 | constexpr int f = (__builtin_assume_dereferenceable((char*)&b + 1, 3), 12);
      |                   ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 errors generated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

after making it constexpr

constexpr_assume_dereferenceable.cpp:5:55: note: cast that performs the conversions of a reinterpret_cast is not allowed in a constant expression
    5 |   constexpr int f = (__builtin_assume_dereferenceable((char*)&b + 1, 3), 12);

Copy link
Contributor

Choose a reason for hiding this comment

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

Casts like that are allowed in non-constant contexts and I believe they can also happen when in __builtin_constant_p, so usually they might make their way into such a builtin evaluation function.

You have to leave f const (but not constexpr) and run clang bin/clang++ -c array.cpp -std=c++2c -S -emit-llvm -o - to see that we get a global initializer because f cannot be initialized at compile time.


constexpr bool test_range_valid() {
constexpr int range_data[5] = {1, 2, 3, 4, 5};
__builtin_assume_dereferenceable(range_data, 5 * sizeof(int));
return range_data[0] == 1;
}
static_assert(test_range_valid(), "");

constexpr bool test_range_invalid() { // expected-error {{constexpr function never produces a constant expression}}
constexpr int range_data[5] = {1, 2, 3, 4, 5};
__builtin_assume_dereferenceable(range_data, 6 * sizeof(int)); // expected-note 2{{read of dereferenced one-past-the-end pointer is not allowed in a constant expression}}
return true;
}
static_assert(test_range_invalid(), ""); // expected-error {{not an integral constant expression}} expected-note {{in call to}}

constexpr int arr1[10] = {};
constexpr int valid = (__builtin_assume_dereferenceable(arr1, 40), 12);

constexpr int invalid = (__builtin_assume_dereferenceable((int*)123, 4), 12); // expected-error {{constexpr variable 'invalid' must be initialized by a constant expression}} expected-note {{cast that performs the conversions of a reinterpret_cast is not allowed in a constant expression}}

constexpr int arr2[5] = {1, 2, 3, 4, 5};
constexpr int too_large = (__builtin_assume_dereferenceable(arr2, 6 * sizeof(int)), 12); // expected-error {{constexpr variable 'too_large' must be initialized by a constant expression}} expected-note {{read of dereferenced one-past-the-end pointer is not allowed in a constant expression}}

constexpr int null = (__builtin_assume_dereferenceable(nullptr, 4), 12); // expected-error {{constexpr variable 'null' must be initialized by a constant expression}} expected-note {{read of dereferenced null pointer is not allowed in a constant expression}}