[clang] [clang][OpenCL] Do not emit UBSan divrem checks in OpenCL C (PR #229710)
Igor Gorban via cfe-commits
cfe-commits at lists.llvm.org
Wed Oct 7 02:39:40 PDT 2026
https://github.com/igorban-intel created https://github.com/llvm/llvm-project/pull/229710
OpenCL C v3.0 s6.5.1 states that integer division by zero and integer
division whose result lies outside the range of the type (INT_MIN / -1)
do not cause an exception but result in an unspecified value. Neither
is undefined behavior, so -fsanitize=integer-divide-by-zero and the
division check of -fsanitize=signed-integer-overflow must not report
them. Skip the check for integer / and % when the input language is
OpenCL C or C++ for OpenCL, the same way the shift exponent check is
already skipped there.
The specification names only division; % is treated the same way,
since C states the zero-divisor case for both operators and makes
INT_MIN % -1 undefined only because INT_MIN / -1 is.
Signed overflow checks for +, - and * are unaffected.
Assisted-by: Claude Opus 5.5
>From c99f8985630886067d88348f37d839de3ebf9cd5 Mon Sep 17 00:00:00 2001
From: "Gorban, Igor" <igor.gorban at intel.com>
Date: Wed, 7 Oct 2026 11:05:25 +0200
Subject: [PATCH] [clang][OpenCL] Do not emit UBSan divrem checks in OpenCL C
OpenCL C v3.0 s6.5.1 states that integer division by zero and integer
division whose result lies outside the range of the type (INT_MIN / -1)
do not cause an exception but result in an unspecified value. Neither
is undefined behavior, so -fsanitize=integer-divide-by-zero and the
division check of -fsanitize=signed-integer-overflow must not report
them. Skip the check for integer / and % when the input language is
OpenCL C or C++ for OpenCL, the same way the shift exponent check is
already skipped there.
The specification names only division; % is treated the same way,
since C states the zero-divisor case for both operators and makes
INT_MIN % -1 undefined only because INT_MIN / -1 is.
Signed overflow checks for +, - and * are unaffected.
Assisted-by: Claude Opus 5.5
---
clang/docs/ReleaseNotes.md | 8 ++
clang/lib/CodeGen/CGExprScalar.cpp | 9 +-
clang/test/CodeGenOpenCL/ubsan-divrem.cl | 140 +++++++++++++++++++++++
3 files changed, 155 insertions(+), 2 deletions(-)
create mode 100644 clang/test/CodeGenOpenCL/ubsan-divrem.cl
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 08442f74879af..8991e7ccb7667 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -920,6 +920,14 @@ features cannot lower the translation-unit ABI level;
``cl_khr_subgroup_shuffle``, and ``cl_khr_subgroup_shuffle_relative`` are
promoted to core features in OpenCL C 3.1. A target claiming OpenCL C 3.1
conformance without supporting one of these features is now diagnosed.
+- ``-fsanitize=integer-divide-by-zero`` and the division check of
+ ``-fsanitize=signed-integer-overflow`` no longer instrument integer ``/`` and
+ ``%`` in OpenCL C and C++ for OpenCL. OpenCL C defines integer division by
+ zero and ``INT_MIN / -1`` to yield an unspecified value without raising an
+ exception (OpenCL C v3.0 s6.5.1). The specification names only division;
+ ``%`` is treated the same way, since C states the zero-divisor case for both
+ operators (C99 6.5.5p5) and makes ``INT_MIN % -1`` undefined only because
+ ``INT_MIN / -1`` is (C11 6.5.5p6).
### Target Specific Changes
diff --git a/clang/lib/CodeGen/CGExprScalar.cpp b/clang/lib/CodeGen/CGExprScalar.cpp
index 7ecaa149f7b34..a4ceee62dfc7c 100644
--- a/clang/lib/CodeGen/CGExprScalar.cpp
+++ b/clang/lib/CodeGen/CGExprScalar.cpp
@@ -4320,7 +4320,10 @@ Value *ScalarExprEmitter::EmitDiv(const BinOpInfo &Ops) {
SanitizerKind::SO_SignedIntegerOverflow,
SanitizerKind::SO_FloatDivideByZero},
SanitizerHandler::DivremOverflow);
- if ((CGF.SanOpts.has(SanitizerKind::IntegerDivideByZero) ||
+ // OpenCL C v3.0 s6.5.1: integer division by zero and INT_MIN / -1 yield an
+ // unspecified value, not undefined behavior.
+ if (!CGF.getLangOpts().OpenCL &&
+ (CGF.SanOpts.has(SanitizerKind::IntegerDivideByZero) ||
CGF.SanOpts.has(SanitizerKind::SignedIntegerOverflow)) &&
Ops.Ty->isIntegerType() &&
(Ops.mayHaveIntegerDivisionByZero() || Ops.mayHaveIntegerOverflow())) {
@@ -4369,7 +4372,9 @@ Value *ScalarExprEmitter::EmitDiv(const BinOpInfo &Ops) {
Value *ScalarExprEmitter::EmitRem(const BinOpInfo &Ops) {
// Rem in C can't be a floating point type: C99 6.5.5p2.
- if ((CGF.SanOpts.has(SanitizerKind::IntegerDivideByZero) ||
+ // OpenCL: treated like integer division, see EmitDiv.
+ if (!CGF.getLangOpts().OpenCL &&
+ (CGF.SanOpts.has(SanitizerKind::IntegerDivideByZero) ||
CGF.SanOpts.has(SanitizerKind::SignedIntegerOverflow)) &&
Ops.Ty->isIntegerType() &&
(Ops.mayHaveIntegerDivisionByZero() || Ops.mayHaveIntegerOverflow())) {
diff --git a/clang/test/CodeGenOpenCL/ubsan-divrem.cl b/clang/test/CodeGenOpenCL/ubsan-divrem.cl
new file mode 100644
index 0000000000000..cab30708e51e9
--- /dev/null
+++ b/clang/test/CodeGenOpenCL/ubsan-divrem.cl
@@ -0,0 +1,140 @@
+// OpenCL C v3.0 s6.5.1 defines both conditions that
+// __ubsan_handle_divrem_overflow diagnoses: integer division whose result lies
+// outside the range of the integer type -- INT_MIN / -1 -- and integer divide
+// by zero do not cause an exception but result in an unspecified value. Clang
+// therefore does not emit the check in OpenCL C or C++ for OpenCL, the same way
+// it does not emit -fsanitize=shift-exponent there.
+//
+// Overflow of +, - and * is not covered by that wording, so the arithmetic
+// controls must remain instrumented whenever signed-integer-overflow is
+// enabled.
+//
+// The KERNEL/GLOBAL macros come from the command line so that the same source
+// can be compiled as OpenCL C, as C++ for OpenCL, as C and as SYCL device code.
+// Every run passes -disable-llvm-passes so the checks see the frontend's own
+// output rather than the output of the default OpenCL -O2 pipeline.
+
+// OpenCL C omits divrem checks while retaining arithmetic checks.
+// RUN: %clang_cc1 %s -triple spir64-unknown-unknown -emit-llvm -o - \
+// RUN: -disable-llvm-passes -DKERNEL=__kernel -DGLOBAL=__global \
+// RUN: -fsanitize=integer-divide-by-zero,signed-integer-overflow \
+// RUN: -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefixes=CHECK,ARITH \
+// RUN: --implicit-check-not=__ubsan_handle_divrem_overflow
+//
+// C++ for OpenCL omits divrem checks while retaining arithmetic checks.
+// RUN: %clang_cc1 %s -triple spir64-unknown-unknown -cl-std=clc++2021 \
+// RUN: -emit-llvm -disable-llvm-passes -o - \
+// RUN: -DKERNEL=__kernel -DGLOBAL=__global \
+// RUN: -fsanitize=integer-divide-by-zero,signed-integer-overflow \
+// RUN: -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefixes=CHECK,ARITH \
+// RUN: --implicit-check-not=__ubsan_handle_divrem_overflow
+//
+// OpenCL C omits integer-divide-by-zero checks.
+// RUN: %clang_cc1 %s -triple spir64-unknown-unknown -emit-llvm -o - \
+// RUN: -disable-llvm-passes -DKERNEL=__kernel -DGLOBAL=__global \
+// RUN: -fsanitize=integer-divide-by-zero -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefix=CHECK \
+// RUN: --implicit-check-not=__ubsan_handle_divrem_overflow
+//
+// OpenCL C omits signed-overflow divrem checks but retains arithmetic checks.
+// RUN: %clang_cc1 %s -triple spir64-unknown-unknown -emit-llvm -o - \
+// RUN: -disable-llvm-passes -DKERNEL=__kernel -DGLOBAL=__global \
+// RUN: -fsanitize=signed-integer-overflow -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefixes=CHECK,ARITH \
+// RUN: --implicit-check-not=__ubsan_handle_divrem_overflow
+//
+// C retains both divrem and arithmetic checks.
+// RUN: %clang_cc1 %s -x c -triple x86_64-unknown-linux-gnu -emit-llvm -o - \
+// RUN: -disable-llvm-passes -DKERNEL= -DGLOBAL= \
+// RUN: -fsanitize=integer-divide-by-zero,signed-integer-overflow \
+// RUN: -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefixes=CHECK,ARITH,SIGNED,UNSIGNED
+//
+// SYCL device C++ retains both divrem and arithmetic checks.
+// RUN: %clang_cc1 %s -x c++ -fsycl-is-device -triple spir64-unknown-unknown \
+// RUN: -emit-llvm -disable-llvm-passes -o - \
+// RUN: -DKERNEL='[[clang::sycl_external]]' -DGLOBAL= \
+// RUN: -fsanitize=integer-divide-by-zero,signed-integer-overflow \
+// RUN: -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefixes=CHECK,ARITH,SIGNED,UNSIGNED
+//
+// C retains integer-divide-by-zero checks.
+// RUN: %clang_cc1 %s -x c -triple x86_64-unknown-linux-gnu -emit-llvm -o - \
+// RUN: -disable-llvm-passes -DKERNEL= -DGLOBAL= \
+// RUN: -fsanitize=integer-divide-by-zero -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefixes=CHECK,SIGNED,UNSIGNED
+//
+// C retains signed-overflow divrem and arithmetic checks.
+// RUN: %clang_cc1 %s -x c -triple x86_64-unknown-linux-gnu -emit-llvm -o - \
+// RUN: -disable-llvm-passes -DKERNEL= -DGLOBAL= \
+// RUN: -fsanitize=signed-integer-overflow -fsanitize-minimal-runtime \
+// RUN: | FileCheck %s --check-prefixes=CHECK,ARITH,SIGNED
+
+// Signed: both the divide-by-zero and the INT_MIN / -1 arm would apply.
+// CHECK-LABEL: divrem_signed
+// SIGNED: call{{.*}} @__ubsan_handle_divrem_overflow
+// CHECK: sdiv i32
+// SIGNED: call{{.*}} @__ubsan_handle_divrem_overflow
+// CHECK: srem i32
+KERNEL void divrem_signed(GLOBAL int *a, GLOBAL int *b, GLOBAL int *out) {
+ out[0] = a[0] / b[0];
+ out[1] = a[0] % b[0];
+}
+
+// Unsigned: only the divide-by-zero arm would apply.
+// CHECK-LABEL: divrem_unsigned
+// UNSIGNED: call{{.*}} @__ubsan_handle_divrem_overflow
+// CHECK: udiv i32
+// UNSIGNED: call{{.*}} @__ubsan_handle_divrem_overflow
+// CHECK: urem i32
+KERNEL void divrem_unsigned(GLOBAL unsigned int *a, GLOBAL unsigned int *b,
+ GLOBAL unsigned int *out) {
+ out[0] = a[0] / b[0];
+ out[1] = a[0] % b[0];
+}
+
+// Compound assignment reaches the same emission path. This is the form the
+// OpenCL-CTS integer_divideAssign and integer_moduloAssign kernels use.
+// CHECK-LABEL: compound_divrem
+// SIGNED: call{{.*}} @__ubsan_handle_divrem_overflow
+// CHECK: sdiv i32
+// SIGNED: call{{.*}} @__ubsan_handle_divrem_overflow
+// CHECK: srem i32
+KERNEL void compound_divrem(GLOBAL int *a, GLOBAL int *out) {
+ out[0] /= a[0];
+ out[1] %= a[0];
+}
+
+// Integer vectors are not instrumented today: the check requires a scalar
+// integer type. The suppressed runs still must not find a handler here, so a
+// later change that instruments vectors outside the guarded path fails this
+// test. The OpenCL-CTS integer_ops kernels run every vector width.
+// CHECK-LABEL: vector_divrem
+// CHECK: sdiv <4 x i32>
+// CHECK: srem <4 x i32>
+typedef int vint4 __attribute__((ext_vector_type(4)));
+KERNEL void vector_divrem(GLOBAL vint4 *a, GLOBAL vint4 *b, GLOBAL vint4 *out) {
+ out[0] = a[0] / b[0];
+ out[1] = a[0] % b[0];
+}
+
+// Controls: signed overflow in +, - and * must still be checked.
+// CHECK-LABEL: add_still_checked
+// ARITH: call{{.*}} @__ubsan_handle_add_overflow
+KERNEL void add_still_checked(GLOBAL int *a, GLOBAL int *b, GLOBAL int *out) {
+ out[0] = a[0] + b[0];
+}
+
+// CHECK-LABEL: sub_still_checked
+// ARITH: call{{.*}} @__ubsan_handle_sub_overflow
+KERNEL void sub_still_checked(GLOBAL int *a, GLOBAL int *b, GLOBAL int *out) {
+ out[0] = a[0] - b[0];
+}
+
+// CHECK-LABEL: mul_still_checked
+// ARITH: call{{.*}} @__ubsan_handle_mul_overflow
+KERNEL void mul_still_checked(GLOBAL int *a, GLOBAL int *b, GLOBAL int *out) {
+ out[0] = a[0] * b[0];
+}
More information about the cfe-commits
mailing list