[clang] [analyzer] Fix false positive for bitfield read after cast-pointer write (PR #188387)

via cfe-commits cfe-commits at lists.llvm.org
Wed Mar 25 20:01:01 PDT 2026


https://github.com/earnol updated https://github.com/llvm/llvm-project/pull/188387

>From 421989e03206feaf8d138d7ad648dae2d0f499a7 Mon Sep 17 00:00:00 2001
From: Vladislav Aranov <vladislav.aranov at ericsson.com>
Date: Wed, 25 Mar 2026 01:36:02 +0100
Subject: [PATCH] [analyzer] Fix false positive for bitfield read after
 cast-pointer write

RegionStore incorrectly reported "Assigned value is uninitialized" when
reading a bitfield member after the containing struct was written through
a cast pointer (e.g. *(unsigned*)&s = val). The write created a direct
scalar binding on the super-region, but when reading a bitfield
sub-region, the store found neither a direct FieldRegion binding nor the
original default zero binding being displaced by the write, and causing
fallback to UndefinedVal.

Fix: Track per-field bit coverage using a bitmask stored as a ConcreteInt
direct binding (with bit width FWidth + 1 to distinguish it from real
values). On each cast-pointer write, compute which bits of each
overlapping bitfield are covered and merge with any existing bitmask.
When all bits of a field are covered the bitmask is replaced with
UnknownVal. On read, a partial bitmask is treated as UndefinedVal (some
bits are still uninitialized). This correctly handles multiple partial
writes that collectively cover a field, without affecting fields beyond
the write or regular field assignments.
---
 clang/lib/StaticAnalyzer/Core/RegionStore.cpp |  94 ++++++++++++-
 clang/test/Analysis/bitfield-cast-write.c     | 130 ++++++++++++++++++
 2 files changed, 222 insertions(+), 2 deletions(-)
 create mode 100644 clang/test/Analysis/bitfield-cast-write.c

diff --git a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
index 6ec66298e8c45..139a8943a939f 100644
--- a/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
+++ b/clang/lib/StaticAnalyzer/Core/RegionStore.cpp
@@ -519,6 +519,13 @@ class RegionStoreManager : public StoreManager {
     return StateMgr.getOwningEngine().getAnalysisManager().options;
   }
 
+  /// Update per-field bit-coverage bitmasks for bitfields overlapping a
+  /// cast-pointer write to a record region.
+  LimitedRegionBindingsRef
+  updateBitfieldCoverage(LimitedRegionBindingsConstRef B,
+                         LimitedRegionBindingsRef NewB, const MemRegion *R,
+                         SVal V);
+
 public:
   RegionStoreManager(ProgramStateManager &mgr)
       : StoreManager(mgr), RBFactory(mgr.getAllocator()),
@@ -1336,7 +1343,8 @@ void InvalidateRegionsWorker::VisitCluster(const MemRegion *baseR,
       if (!NumElements) // We are not dealing with a constant size array
         goto conjure_default;
       QualType ElementTy = AT->getElementType();
-      uint64_t ElemSize = Ctx.getTypeSize(ElementTy);
+      uint64_t ElemSize =
+          Ctx.getTypeSizeInChars(ElementTy).getQuantity() * Ctx.getCharWidth();
       const RegionOffset &RO = baseR->getAsOffset();
       const MemRegion *SuperR = baseR->getBaseRegion();
       if (RO.hasSymbolicOffset()) {
@@ -2037,6 +2045,30 @@ SVal RegionStoreManager::getSValFromStringLiteral(const StringLiteral *SL,
   return svalBuilder.makeIntVal(Code, ElemT);
 }
 
+/// Check whether V is a partial-coverage bitmask for a bitfield of
+/// width FieldWidth. Such bitmasks are stored as ConcreteInt values
+/// with bit width FieldWidth + 1 to distinguish them from real values.
+static bool isBitfieldCoverageMask(SVal V, unsigned FieldWidth) {
+  if (auto CI = V.getAs<nonloc::ConcreteInt>())
+    return CI->getValue()->getBitWidth() == FieldWidth + 1;
+  return false;
+}
+
+/// Merge NewBits into the existing coverage bitmask for a bitfield and
+/// return the appropriate SVal: UnknownVal if all bits are now covered,
+/// or an updated bitmask ConcreteInt otherwise.
+static SVal mergeBitfieldCoverage(llvm::APInt NewBits, unsigned FieldWidth,
+                                  std::optional<SVal> Existing,
+                                  SValBuilder &SVB) {
+  if (Existing && isBitfieldCoverageMask(*Existing, FieldWidth))
+    NewBits |= *Existing->getAs<nonloc::ConcreteInt>()->getValue().get();
+
+  llvm::APInt AllBits = llvm::APInt::getLowBitsSet(FieldWidth + 1, FieldWidth);
+  if ((NewBits & AllBits) == AllBits)
+    return UnknownVal();
+  return SVB.makeIntVal(llvm::APSInt(NewBits, true));
+}
+
 static std::optional<SVal> getDerivedSymbolForBinding(
     RegionBindingsConstRef B, const TypedValueRegion *BaseRegion,
     const TypedValueRegion *SubReg, const ASTContext &Ctx, SValBuilder &SVB) {
@@ -2117,8 +2149,13 @@ SVal RegionStoreManager::getBindingForField(RegionBindingsConstRef B,
                                             const FieldRegion* R) {
 
   // Check if the region has a binding.
-  if (const std::optional<SVal> &V = B.getDirectBinding(R))
+  if (const std::optional<SVal> &V = B.getDirectBinding(R)) {
+    // A partial-coverage bitmask means some bits are still uninitialized.
+    if (R->getDecl()->isBitField() &&
+        isBitfieldCoverageMask(*V, R->getDecl()->getBitWidthValue()))
+      return UndefinedVal();
     return *V;
+  }
 
   // If the containing record was initialized, try to get its constant value.
   const FieldDecl *FD = R->getDecl();
@@ -2495,6 +2532,54 @@ bool RegionStoreManager::includedInBindings(Store store,
   return false;
 }
 
+LimitedRegionBindingsRef
+RegionStoreManager::updateBitfieldCoverage(LimitedRegionBindingsConstRef B,
+                                           LimitedRegionBindingsRef NewB,
+                                           const MemRegion *R, SVal V) {
+  if (isa<FieldRegion>(R))
+    return NewB;
+  const auto *BaseR = dyn_cast<TypedValueRegion>(R->getBaseRegion());
+  if (!BaseR || BaseR == R || !BaseR->getValueType()->isRecordType())
+    return NewB;
+  const auto *RD = BaseR->getValueType()->getAsRecordDecl();
+  if (!RD)
+    return NewB;
+  RegionOffset ROffset = R->getAsOffset();
+  if (!ROffset.isValid() || ROffset.hasSymbolicOffset() ||
+      ROffset.getRegion() != BaseR)
+    return NewB;
+  QualType VTy = V.getType(Ctx);
+  if (VTy.isNull())
+    return NewB;
+
+  uint64_t WriteWidth = Ctx.getTypeSize(VTy);
+  int64_t WriteBegin = ROffset.getOffset();
+  uint64_t WriteEnd = WriteBegin + WriteWidth;
+
+  for (const auto *FD : RD->fields()) {
+    if (!FD->isBitField())
+      continue;
+    uint64_t FOffset = Ctx.getFieldOffset(FD);
+    uint64_t FWidth = FD->getBitWidthValue();
+    uint64_t FEnd = FOffset + FWidth;
+    if ((uint64_t)WriteBegin >= FEnd || WriteEnd <= FOffset)
+      continue;
+
+    uint64_t CovBegin =
+        (uint64_t)WriteBegin > FOffset ? (uint64_t)WriteBegin - FOffset : 0;
+    uint64_t CovEnd = WriteEnd < FEnd ? WriteEnd - FOffset : FWidth;
+    llvm::APInt NewBits = llvm::APInt::getBitsSet(FWidth + 1, CovBegin, CovEnd);
+
+    // Merge with existing bitmask (read from B, not NewB, because
+    // removeSubRegionBindings may have cleared it).
+    const FieldRegion *FR = MRMgr.getFieldRegion(FD, BaseR);
+    SVal Merged = mergeBitfieldCoverage(NewBits, FWidth, B.getDirectBinding(FR),
+                                        svalBuilder);
+    NewB = NewB.addBinding(BindingKey::Make(FR, BindingKey::Direct), Merged);
+  }
+  return NewB;
+}
+
 //===----------------------------------------------------------------------===//
 // Binding values to regions.
 //===----------------------------------------------------------------------===//
@@ -2554,6 +2639,11 @@ RegionStoreManager::bind(LimitedRegionBindingsConstRef B, Loc L, SVal V) {
   // Clear out bindings that may overlap with this binding.
   auto NewB = removeSubRegionBindings(B, cast<SubRegion>(R));
 
+  // When writing a scalar through a cast pointer to a record region
+  // (e.g. *(unsigned*)&struct_var = val), track which bits of each
+  // overlapping bitfield have been written using a per-field bitmask.
+  NewB = updateBitfieldCoverage(B, NewB, R, V);
+
   // LazyCompoundVals should be always bound as 'default' bindings.
   auto KeyKind = isa<nonloc::LazyCompoundVal>(V) ? BindingKey::Default
                                                  : BindingKey::Direct;
diff --git a/clang/test/Analysis/bitfield-cast-write.c b/clang/test/Analysis/bitfield-cast-write.c
new file mode 100644
index 0000000000000..45d838c08dc55
--- /dev/null
+++ b/clang/test/Analysis/bitfield-cast-write.c
@@ -0,0 +1,130 @@
+// RUN: %clang_analyze_cc1 -triple x86_64-unknown-linux-gnu -analyzer-checker=core -verify %s
+// Regression test: writing to a bitfield struct through a cast pointer
+// should not cause false "uninitialized value" reports on bitfield reads.
+
+typedef struct {
+  unsigned pad : 4;
+  unsigned val : 4;
+} S1;
+
+void write_through_cast(unsigned *out) {
+  *out = 42;
+}
+
+void bitfield_read_after_cast_write_via_callee(unsigned char *out) {
+  S1 s = {0};
+  write_through_cast((unsigned *)&s);
+  *out = s.val; // no-warning
+}
+
+void bitfield_read_after_direct_cast_write(unsigned char *out) {
+  S1 s = {0};
+  *(unsigned *)&s = 0xFF;
+  *out = s.val; // no-warning
+}
+
+#pragma pack(push, 1)
+typedef struct {
+  unsigned pad : 4;
+  unsigned val : 4;
+  unsigned rest : 24;
+} S1Packed;
+#pragma pack(pop)
+
+void packed_bitfield_read_after_cast_write_via_callee(unsigned char *out) {
+  S1Packed s = {0};
+  write_through_cast((unsigned *)&s);
+  *out = s.val; // no-warning
+}
+
+void packed_bitfield_read_after_direct_cast_write(unsigned char *out) {
+  S1Packed s = {0};
+  *(unsigned *)&s = 0xFF;
+  *out = s.val; // no-warning
+}
+
+// Ensure a real uninitialized read is still detected when the cast write
+// only covers the first 4 bytes but the bitfield lives beyond that.
+typedef struct {
+  unsigned first : 32;
+  unsigned second : 32;
+} S2;
+
+void partial_cast_write_leaves_tail_uninitialized(unsigned *out) {
+  S2 s;
+  *(unsigned *)&s = 0xFF; // only writes first 4 bytes
+  *out = s.second; // expected-warning{{Assigned value is uninitialized}}
+}
+
+// Another positive case
+#pragma pack(push, 1)
+typedef struct {
+  unsigned first : 16;
+  unsigned f1 : 8;
+  unsigned f2 : 16;
+  unsigned second : 32;
+  unsigned third : 32;
+} S2Packed;
+#pragma pack(pop)
+
+void partial_cast_write_partial_overlap_uninitialized(unsigned *out) {
+  S2Packed s;
+  *(unsigned *)&s = 0xFEDCBA98; // only writes first 4 bytes
+  *out = s.f2; // expected-warning{{Assigned value is uninitialized}}
+}
+
+void partial_cast_write_beyond_binding_uninitialized(unsigned *out) {
+  S2Packed s;
+  *(unsigned *)&s = 0xFEDCBA98; // only writes first 4 bytes
+  *out = s.second; // expected-warning{{Assigned value is uninitialized}}
+}
+
+void partial_cast_write_beyond_shifted_uninitialized(unsigned *out) {
+  S2Packed s;
+  *(unsigned *)((char *)&s + 3)= 0xFEDCBA98; // only writes 4 bytes with shift
+  *out = s.f2; // no-warning
+}
+
+void partial_cast_write_beyond_shifted_uninitialized2(unsigned *out) {
+  S2Packed s;
+  *(unsigned *)((char *)&s + 4)= 0xFEDCBA98; // only writes 4 bytes with shift
+  *out = s.f2; // expected-warning{{Assigned value is uninitialized}}
+}
+
+void partial_cast_write_beyond_shifted_uninitialized3(unsigned *out) {
+  S2Packed s;
+  *(unsigned *)((char *)&s + 5)= 0xFEDCBA98; // only writes 4 bytes with shift
+  *out = s.second; // no-warning
+}
+
+void partial_cast_write_beyond_shifted_uninitialized4(unsigned *out) {
+  S2Packed s;
+  *(unsigned *)((char *)&s + 5)= 0xFEDCBA98; // only writes 4 bytes with shift
+  *out = s.third; // expected-warning{{Assigned value is uninitialized}}
+}
+
+
+void partial_cast_write_beyond_gap_uninitialized1(unsigned *out, unsigned *out2) {
+  S2Packed s;
+  *(unsigned short *)(&s)= 0xFEDC; // only writes 2 bytes in beginning
+  *(unsigned *)((char *)&s + 5)= 0xFEDCBA98; // only writes 4 bytes with shift
+  *out2 = s.second; // no-warning
+  *out = s.f2; // expected-warning{{Assigned value is uninitialized}}
+}
+
+void partial_cast_write_beyond_gap_uninitialized2(unsigned *out, unsigned *out2) {
+  S2Packed s;
+  *(unsigned short *)(&s)= 0xFEDC; // only writes 2 bytes in beginning
+  *(unsigned *)((char *)&s + 2)= 0xFEDCBA98; // only writes 4 bytes with shift
+  *out = s.f2; // no-warning
+  *out2 = s.second; // expected-warning{{Assigned value is uninitialized}}
+}
+
+void partial_cast_write_beyond_overlapped_init(unsigned *out, unsigned *out2) {
+  S2Packed s;
+  *(unsigned short *)(&s)= 0xFEDC; // only writes 2 bytes in beginning
+  *(unsigned *)((char *)&s + 7)= 0xFEDCBA98; // only writes 4 bytes with shift
+  *(unsigned *)((char *)&s + 3)= 0x76543210; // only writes 4 bytes with shift
+  *out2 = s.second; // no-warning
+  *out = s.f1; // expected-warning{{Assigned value is uninitialized}}
+}



More information about the cfe-commits mailing list