[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:10:36 PDT 2026
https://github.com/earnol updated https://github.com/llvm/llvm-project/pull/188387
>From 7461d7c139f0a56ce2d72cad692c02efbe396993 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 | 91 +++++++++++-
clang/test/Analysis/bitfield-cast-write.c | 130 ++++++++++++++++++
2 files changed, 220 insertions(+), 1 deletion(-)
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..2d80956965531 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()),
@@ -2037,6 +2044,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 +2148,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 +2531,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 +2638,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