[clang] [BoundsSafety][APINotes] Upstream API notes format for bounds-safety function parameters (PR #185257)

Connector Switch via cfe-commits cfe-commits at lists.llvm.org
Sun Mar 8 08:42:15 PDT 2026


https://github.com/c8ef updated https://github.com/llvm/llvm-project/pull/185257

>From 19b48fcbf1a7834d8bdd15c2374addfefd611039 Mon Sep 17 00:00:00 2001
From: c8ef <c8ef at outlook.com>
Date: Sun, 8 Mar 2026 15:07:08 +0800
Subject: [PATCH 1/3] implement apinotes for bound safety

---
 clang/include/clang/APINotes/Types.h          | 75 ++++++++++++++++++-
 clang/lib/APINotes/APINotesFormat.h           |  2 +-
 clang/lib/APINotes/APINotesReader.cpp         | 28 ++++++-
 clang/lib/APINotes/APINotesTypes.cpp          | 30 ++++++++
 clang/lib/APINotes/APINotesWriter.cpp         | 35 ++++++++-
 clang/lib/APINotes/APINotesYAMLCompiler.cpp   | 45 +++++++++++
 .../Inputs/Headers/BoundsUnsafe.apinotes      | 38 ++++++++++
 .../APINotes/Inputs/Headers/BoundsUnsafe.h    |  5 ++
 .../APINotes/Inputs/Headers/module.modulemap  |  4 +
 clang/test/APINotes/bounds-safety.c           | 19 +++++
 10 files changed, 276 insertions(+), 5 deletions(-)
 create mode 100644 clang/test/APINotes/Inputs/Headers/BoundsUnsafe.apinotes
 create mode 100644 clang/test/APINotes/Inputs/Headers/BoundsUnsafe.h
 create mode 100644 clang/test/APINotes/bounds-safety.c

diff --git a/clang/include/clang/APINotes/Types.h b/clang/include/clang/APINotes/Types.h
index fb2b91a3e1750..f6b8b855218f9 100644
--- a/clang/include/clang/APINotes/Types.h
+++ b/clang/include/clang/APINotes/Types.h
@@ -338,6 +338,71 @@ inline bool operator!=(const ContextInfo &LHS, const ContextInfo &RHS) {
   return !(LHS == RHS);
 }
 
+/// API notes for bounds safety annotations
+class BoundsSafetyInfo {
+public:
+  enum class BoundsSafetyKind {
+    CountedBy = 0,
+    CountedByOrNull,
+    SizedBy,
+    SizedByOrNull,
+    EndedBy,
+  };
+
+private:
+  /// Whether this property has been audited for nullability.
+  LLVM_PREFERRED_TYPE(bool)
+  unsigned KindAudited : 1;
+
+  /// The kind of nullability for this property. Only valid if the nullability
+  /// has been audited.
+  LLVM_PREFERRED_TYPE(BoundsSafetyKind)
+  unsigned Kind : 3;
+
+  LLVM_PREFERRED_TYPE(bool)
+  unsigned LevelAudited : 1;
+
+  unsigned Level : 3;
+
+public:
+  std::string ExternalBounds;
+
+  BoundsSafetyInfo()
+      : KindAudited(false), Kind(0), LevelAudited(false), Level(0),
+        ExternalBounds("") {}
+
+  std::optional<BoundsSafetyKind> getKind() const {
+    return KindAudited ? std::optional<BoundsSafetyKind>(
+                             static_cast<BoundsSafetyKind>(Kind))
+                       : std::nullopt;
+  }
+
+  void setKindAudited(BoundsSafetyKind kind) {
+    KindAudited = true;
+    Kind = static_cast<unsigned>(kind);
+  }
+
+  std::optional<unsigned> getLevel() const {
+    return LevelAudited ? std::optional<unsigned>(Level) : std::nullopt;
+  }
+
+  void setLevelAudited(unsigned level) {
+    LevelAudited = true;
+    Level = level;
+  }
+
+  friend bool operator==(const BoundsSafetyInfo &, const BoundsSafetyInfo &);
+
+  LLVM_DUMP_METHOD void dump(llvm::raw_ostream &OS) const;
+};
+
+inline bool operator==(const BoundsSafetyInfo &LHS,
+                       const BoundsSafetyInfo &RHS) {
+  return LHS.KindAudited == RHS.KindAudited && LHS.Kind == RHS.Kind &&
+         LHS.LevelAudited == RHS.LevelAudited && LHS.Level == RHS.Level &&
+         LHS.ExternalBounds == RHS.ExternalBounds;
+}
+
 /// API notes for a variable/property.
 class VariableInfo : public CommonEntityInfo {
   /// Whether this property has been audited for nullability.
@@ -477,10 +542,12 @@ class ParamInfo : public VariableInfo {
   unsigned RawRetainCountConvention : 3;
 
 public:
+  std::optional<BoundsSafetyInfo> BoundsSafety;
+
   ParamInfo()
       : NoEscapeSpecified(false), NoEscape(false),
         LifetimeboundSpecified(false), Lifetimebound(false),
-        RawRetainCountConvention() {}
+        RawRetainCountConvention(), BoundsSafety(std::nullopt) {}
 
   std::optional<bool> isNoEscape() const {
     return NoEscapeSpecified ? std::optional<bool>(NoEscape) : std::nullopt;
@@ -526,6 +593,9 @@ class ParamInfo : public VariableInfo {
     if (!RawRetainCountConvention)
       RawRetainCountConvention = RHS.RawRetainCountConvention;
 
+    if (!BoundsSafety)
+      BoundsSafety = RHS.BoundsSafety;
+
     return *this;
   }
 
@@ -540,7 +610,8 @@ inline bool operator==(const ParamInfo &LHS, const ParamInfo &RHS) {
          LHS.NoEscape == RHS.NoEscape &&
          LHS.LifetimeboundSpecified == RHS.LifetimeboundSpecified &&
          LHS.Lifetimebound == RHS.Lifetimebound &&
-         LHS.RawRetainCountConvention == RHS.RawRetainCountConvention;
+         LHS.RawRetainCountConvention == RHS.RawRetainCountConvention &&
+         LHS.BoundsSafety == RHS.BoundsSafety;
 }
 
 inline bool operator!=(const ParamInfo &LHS, const ParamInfo &RHS) {
diff --git a/clang/lib/APINotes/APINotesFormat.h b/clang/lib/APINotes/APINotesFormat.h
index 9d868a1b4da1f..6b1cc3128ca42 100644
--- a/clang/lib/APINotes/APINotesFormat.h
+++ b/clang/lib/APINotes/APINotesFormat.h
@@ -24,7 +24,7 @@ const uint16_t VERSION_MAJOR = 0;
 /// API notes file minor version number.
 ///
 /// When the format changes IN ANY WAY, this number should be incremented.
-const uint16_t VERSION_MINOR = 38; // SwiftSafety
+const uint16_t VERSION_MINOR = 39; // BoundsSafety
 
 const uint8_t kSwiftConforms = 1;
 const uint8_t kSwiftDoesNotConform = 2;
diff --git a/clang/lib/APINotes/APINotesReader.cpp b/clang/lib/APINotes/APINotesReader.cpp
index f00c7ac1d9d9b..c176f4f316a2e 100644
--- a/clang/lib/APINotes/APINotesReader.cpp
+++ b/clang/lib/APINotes/APINotesReader.cpp
@@ -330,6 +330,28 @@ class FieldTableInfo
   }
 };
 
+void ReadBoundsSafetyInfo(const uint8_t *&Data, BoundsSafetyInfo &Info) {
+  uint8_t Payload = endian::readNext<uint8_t, llvm::endianness::little>(Data);
+
+  if (Payload & 0x01) {
+    uint8_t Level = (Payload >> 1) & 0x7;
+    Info.setLevelAudited(Level);
+  }
+  Payload >>= 4;
+
+  if (Payload & 0x01) {
+    uint8_t Kind = (Payload >> 1) & 0x7;
+    assert(Kind >= (uint8_t)BoundsSafetyInfo::BoundsSafetyKind::CountedBy);
+    assert(Kind <= (uint8_t)BoundsSafetyInfo::BoundsSafetyKind::EndedBy);
+    Info.setKindAudited((BoundsSafetyInfo::BoundsSafetyKind)Kind);
+  }
+
+  uint16_t ExternalBoundsLen =
+      endian::readNext<uint16_t, llvm::endianness::little>(Data);
+  Info.ExternalBounds = std::string(Data, Data + ExternalBoundsLen);
+  Data += ExternalBoundsLen;
+}
+
 /// Read serialized ParamInfo.
 void ReadParamInfo(const uint8_t *&Data, ParamInfo &Info) {
   ReadVariableInfo(Data, Info);
@@ -346,7 +368,11 @@ void ReadParamInfo(const uint8_t *&Data, ParamInfo &Info) {
   if (Payload & 0x01)
     Info.setNoEscape(Payload & 0x02);
   Payload >>= 2;
-  assert(Payload == 0 && "Bad API notes");
+  if (Payload & 0x01) {
+    BoundsSafetyInfo BSI;
+    ReadBoundsSafetyInfo(Data, BSI);
+    Info.BoundsSafety = BSI;
+  }
 }
 
 /// Read serialized FunctionInfo.
diff --git a/clang/lib/APINotes/APINotesTypes.cpp b/clang/lib/APINotes/APINotesTypes.cpp
index bff4be104c6c8..9babd1d9c84dd 100644
--- a/clang/lib/APINotes/APINotesTypes.cpp
+++ b/clang/lib/APINotes/APINotesTypes.cpp
@@ -76,6 +76,34 @@ LLVM_DUMP_METHOD void ObjCPropertyInfo::dump(llvm::raw_ostream &OS) const {
   OS << '\n';
 }
 
+LLVM_DUMP_METHOD void BoundsSafetyInfo::dump(llvm::raw_ostream &OS) const {
+  if (KindAudited) {
+    assert((BoundsSafetyKind)Kind >= BoundsSafetyKind::CountedBy);
+    assert((BoundsSafetyKind)Kind <= BoundsSafetyKind::EndedBy);
+    switch ((BoundsSafetyKind)Kind) {
+    case BoundsSafetyKind::CountedBy:
+      OS << "[counted_by] ";
+      break;
+    case BoundsSafetyKind::CountedByOrNull:
+      OS << "[counted_by_or_null] ";
+      break;
+    case BoundsSafetyKind::SizedBy:
+      OS << "[sized_by] ";
+      break;
+    case BoundsSafetyKind::SizedByOrNull:
+      OS << "[sized_by_or_null] ";
+      break;
+    case BoundsSafetyKind::EndedBy:
+      OS << "[ended_by] ";
+      break;
+    }
+  }
+  if (LevelAudited)
+    OS << "Level: " << Level << " ";
+  OS << "ExternalBounds: "
+     << (ExternalBounds.empty() ? "<missing>" : ExternalBounds) << '\n';
+}
+
 LLVM_DUMP_METHOD void ParamInfo::dump(llvm::raw_ostream &OS) const {
   static_cast<const VariableInfo &>(*this).dump(OS);
   if (NoEscapeSpecified)
@@ -84,6 +112,8 @@ LLVM_DUMP_METHOD void ParamInfo::dump(llvm::raw_ostream &OS) const {
     OS << (Lifetimebound ? "[Lifetimebound] " : "");
   OS << "RawRetainCountConvention: " << RawRetainCountConvention << ' ';
   OS << '\n';
+  if (BoundsSafety)
+    BoundsSafety->dump(OS);
 }
 
 LLVM_DUMP_METHOD void FunctionInfo::dump(llvm::raw_ostream &OS) const {
diff --git a/clang/lib/APINotes/APINotesWriter.cpp b/clang/lib/APINotes/APINotesWriter.cpp
index 390aea57f3915..6da5c5894a7d8 100644
--- a/clang/lib/APINotes/APINotesWriter.cpp
+++ b/clang/lib/APINotes/APINotesWriter.cpp
@@ -1074,14 +1074,45 @@ void APINotesWriter::Implementation::writeGlobalVariableBlock(
 }
 
 namespace {
+void emitBoundsSafetyInfo(raw_ostream &OS, const BoundsSafetyInfo &BSI) {
+  llvm::support::endian::Writer writer(OS, llvm::endianness::little);
+  uint8_t flags = 0;
+  if (auto kind = BSI.getKind()) {
+    flags |= 0x01;                // 1 bit
+    flags |= (uint8_t)*kind << 1; // 3 bits
+  }
+  flags <<= 4;
+  if (auto level = BSI.getLevel()) {
+    flags |= 0x01;        // 1 bit
+    flags |= *level << 1; // 3 bits
+  }
+
+  writer.write<uint8_t>(flags);
+  writer.write<uint16_t>(BSI.ExternalBounds.size());
+  writer.write(
+      ArrayRef<char>{BSI.ExternalBounds.data(), BSI.ExternalBounds.size()});
+}
+
+unsigned getBoundsSafetyInfoSize(const BoundsSafetyInfo &BSI) {
+  return 1 + sizeof(uint16_t) + BSI.ExternalBounds.size();
+}
+
 unsigned getParamInfoSize(const ParamInfo &PI) {
-  return getVariableInfoSize(PI) + 1;
+  unsigned BSISize = 0;
+  /* TO_UPSTREAM(BoundsSafety) ON */
+  if (auto BSI = PI.BoundsSafety)
+    BSISize = getBoundsSafetyInfoSize(*BSI);
+  /* TO_UPSTREAM(BoundsSafety) OFF */
+  return getVariableInfoSize(PI) + 1 + BSISize;
 }
 
 void emitParamInfo(raw_ostream &OS, const ParamInfo &PI) {
   emitVariableInfo(OS, PI);
 
   uint8_t flags = 0;
+  if (PI.BoundsSafety)
+    flags |= 0x01;
+  flags <<= 2;
   if (auto noescape = PI.isNoEscape()) {
     flags |= 0x01;
     if (*noescape)
@@ -1099,6 +1130,8 @@ void emitParamInfo(raw_ostream &OS, const ParamInfo &PI) {
 
   llvm::support::endian::Writer writer(OS, llvm::endianness::little);
   writer.write<uint8_t>(flags);
+  if (auto BSI = PI.BoundsSafety)
+    emitBoundsSafetyInfo(OS, *PI.BoundsSafety);
 }
 
 /// Retrieve the serialized size of the given FunctionInfo, for use in on-disk
diff --git a/clang/lib/APINotes/APINotesYAMLCompiler.cpp b/clang/lib/APINotes/APINotesYAMLCompiler.cpp
index 3b82fdda68af1..7cc3fc2fe14bb 100644
--- a/clang/lib/APINotes/APINotesYAMLCompiler.cpp
+++ b/clang/lib/APINotes/APINotesYAMLCompiler.cpp
@@ -79,6 +79,31 @@ template <> struct ScalarEnumerationTraits<MethodKind> {
 } // namespace yaml
 } // namespace llvm
 
+namespace {
+struct BoundsSafety {
+  BoundsSafetyInfo::BoundsSafetyKind Kind;
+  unsigned Level = 0;
+  StringRef BoundsExpr = "";
+};
+} // namespace
+
+namespace llvm {
+namespace yaml {
+template <> struct ScalarEnumerationTraits<BoundsSafetyInfo::BoundsSafetyKind> {
+  static void enumeration(IO &IO, BoundsSafetyInfo::BoundsSafetyKind &AA) {
+    IO.enumCase(AA, "counted_by",
+                BoundsSafetyInfo::BoundsSafetyKind::CountedBy);
+    IO.enumCase(AA, "counted_by_or_null",
+                BoundsSafetyInfo::BoundsSafetyKind::CountedByOrNull);
+    IO.enumCase(AA, "sized_by", BoundsSafetyInfo::BoundsSafetyKind::SizedBy);
+    IO.enumCase(AA, "sized_by_or_null",
+                BoundsSafetyInfo::BoundsSafetyKind::SizedByOrNull);
+    IO.enumCase(AA, "ended_by", BoundsSafetyInfo::BoundsSafetyKind::EndedBy);
+  }
+};
+} // namespace yaml
+} // namespace llvm
+
 namespace {
 struct Param {
   int Position;
@@ -86,6 +111,7 @@ struct Param {
   std::optional<bool> Lifetimebound = false;
   std::optional<NullabilityKind> Nullability;
   std::optional<RetainCountConventionKind> RetainCountConvention;
+  std::optional<BoundsSafety> BoundsSafety;
   StringRef Type;
 };
 
@@ -137,8 +163,18 @@ template <> struct MappingTraits<Param> {
     IO.mapOptional("NoEscape", P.NoEscape);
     IO.mapOptional("Lifetimebound", P.Lifetimebound);
     IO.mapOptional("Type", P.Type, StringRef(""));
+    IO.mapOptional("BoundsSafety", P.BoundsSafety);
+  }
+};
+
+template <> struct MappingTraits<BoundsSafety> {
+  static void mapping(IO &IO, BoundsSafety &BS) {
+    IO.mapRequired("Kind", BS.Kind);
+    IO.mapRequired("BoundedBy", BS.BoundsExpr);
+    IO.mapOptional("Level", BS.Level, 0);
   }
 };
+
 } // namespace yaml
 } // namespace llvm
 
@@ -787,6 +823,15 @@ class YAMLConverter {
       PI.setLifetimebound(P.Lifetimebound);
       PI.setType(std::string(P.Type));
       PI.setRetainCountConvention(P.RetainCountConvention);
+
+      BoundsSafetyInfo BSI;
+      if (P.BoundsSafety) {
+        BSI.setKindAudited(P.BoundsSafety->Kind);
+        BSI.setLevelAudited(P.BoundsSafety->Level);
+        BSI.ExternalBounds = P.BoundsSafety->BoundsExpr.str();
+      }
+      PI.BoundsSafety = BSI;
+
       if (static_cast<int>(OutInfo.Params.size()) <= P.Position)
         OutInfo.Params.resize(P.Position + 1);
       if (P.Position == -1)
diff --git a/clang/test/APINotes/Inputs/Headers/BoundsUnsafe.apinotes b/clang/test/APINotes/Inputs/Headers/BoundsUnsafe.apinotes
new file mode 100644
index 0000000000000..045e98f92d51b
--- /dev/null
+++ b/clang/test/APINotes/Inputs/Headers/BoundsUnsafe.apinotes
@@ -0,0 +1,38 @@
+---
+Name: BoundsUnsafe
+Functions:
+  - Name: asdf_counted
+    Parameters:
+      - Position: 0
+        BoundsSafety:
+          Kind: counted_by
+          Level: 0
+          BoundedBy: len
+  - Name: asdf_sized
+    Parameters:
+      - Position: 0
+        BoundsSafety:
+          Kind: sized_by
+          Level: 0
+          BoundedBy: size
+  - Name: asdf_counted_n
+    Parameters:
+      - Position: 0
+        BoundsSafety:
+          Kind: counted_by_or_null
+          Level: 0
+          BoundedBy: len
+  - Name: asdf_sized_n
+    Parameters:
+      - Position: 0
+        BoundsSafety:
+          Kind: sized_by_or_null
+          Level: 0
+          BoundedBy: size
+  - Name: asdf_ended
+    Parameters:
+      - Position: 0
+        BoundsSafety:
+          Kind: ended_by
+          Level: 0
+          BoundedBy: end
diff --git a/clang/test/APINotes/Inputs/Headers/BoundsUnsafe.h b/clang/test/APINotes/Inputs/Headers/BoundsUnsafe.h
new file mode 100644
index 0000000000000..1bf8dd50083cc
--- /dev/null
+++ b/clang/test/APINotes/Inputs/Headers/BoundsUnsafe.h
@@ -0,0 +1,5 @@
+void asdf_counted(int *buf, int len);
+void asdf_sized(int *buf, int size);
+void asdf_counted_n(int *buf, int len);
+void asdf_sized_n(int *buf, int size);
+void asdf_ended(int *buf, int *end);
diff --git a/clang/test/APINotes/Inputs/Headers/module.modulemap b/clang/test/APINotes/Inputs/Headers/module.modulemap
index bedb7d505f794..a83844c117fcf 100644
--- a/clang/test/APINotes/Inputs/Headers/module.modulemap
+++ b/clang/test/APINotes/Inputs/Headers/module.modulemap
@@ -1,3 +1,7 @@
+module BoundsUnsafe {
+  header "BoundsUnsafe.h"
+}
+
 module ExternCtx {
   header "ExternCtx.h"
 }
diff --git a/clang/test/APINotes/bounds-safety.c b/clang/test/APINotes/bounds-safety.c
new file mode 100644
index 0000000000000..ccd934ba37762
--- /dev/null
+++ b/clang/test/APINotes/bounds-safety.c
@@ -0,0 +1,19 @@
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/ModulesCache -fapinotes-modules -fsyntax-only -I %S/Inputs/Headers -F %S/Inputs/Frameworks %s -ast-dump -ast-dump-filter asdf | FileCheck %s
+
+#include "BoundsUnsafe.h"
+
+// CHECK: imported in BoundsUnsafe asdf_counted 'void (int *, int)'
+// CHECK: imported in BoundsUnsafe buf 'int *'
+
+// CHECK: imported in BoundsUnsafe asdf_sized 'void (int *, int)'
+// CHECK: imported in BoundsUnsafe buf 'int *'
+
+// CHECK: imported in BoundsUnsafe asdf_counted_n 'void (int *, int)'
+// CHECK: imported in BoundsUnsafe buf 'int *'
+
+// CHECK: imported in BoundsUnsafe asdf_sized_n 'void (int *, int)'
+// CHECK: imported in BoundsUnsafe buf 'int *'
+
+// CHECK: imported in BoundsUnsafe asdf_ended 'void (int *, int *)'
+// CHECK: imported in BoundsUnsafe buf 'int *'

>From 4f0387629dbd7839cabda17ffb7d226190d98a36 Mon Sep 17 00:00:00 2001
From: c8ef <c8ef at outlook.com>
Date: Sun, 8 Mar 2026 15:08:37 +0800
Subject: [PATCH 2/3] implement apinotes for bound safety

---
 clang/lib/APINotes/APINotesWriter.cpp | 2 --
 1 file changed, 2 deletions(-)

diff --git a/clang/lib/APINotes/APINotesWriter.cpp b/clang/lib/APINotes/APINotesWriter.cpp
index 6da5c5894a7d8..35f540a433dcd 100644
--- a/clang/lib/APINotes/APINotesWriter.cpp
+++ b/clang/lib/APINotes/APINotesWriter.cpp
@@ -1099,10 +1099,8 @@ unsigned getBoundsSafetyInfoSize(const BoundsSafetyInfo &BSI) {
 
 unsigned getParamInfoSize(const ParamInfo &PI) {
   unsigned BSISize = 0;
-  /* TO_UPSTREAM(BoundsSafety) ON */
   if (auto BSI = PI.BoundsSafety)
     BSISize = getBoundsSafetyInfoSize(*BSI);
-  /* TO_UPSTREAM(BoundsSafety) OFF */
   return getVariableInfoSize(PI) + 1 + BSISize;
 }
 

>From 5b40f69e3c4acac9496c0893b1fa57502fc5e39e Mon Sep 17 00:00:00 2001
From: c8ef <c8ef at outlook.com>
Date: Sun, 8 Mar 2026 23:42:03 +0800
Subject: [PATCH 3/3] address review comments

---
 clang/include/clang/APINotes/Types.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/include/clang/APINotes/Types.h b/clang/include/clang/APINotes/Types.h
index f6b8b855218f9..7f910034b0cf5 100644
--- a/clang/include/clang/APINotes/Types.h
+++ b/clang/include/clang/APINotes/Types.h
@@ -342,7 +342,7 @@ inline bool operator!=(const ContextInfo &LHS, const ContextInfo &RHS) {
 class BoundsSafetyInfo {
 public:
   enum class BoundsSafetyKind {
-    CountedBy = 0,
+    CountedBy,
     CountedByOrNull,
     SizedBy,
     SizedByOrNull,



More information about the cfe-commits mailing list