[Lldb-commits] [lldb] [lldb][DIL] Validate bitfield extraction ranges (PR #213055)
Yao Qi via lldb-commits
lldb-commits at lists.llvm.org
Thu Jul 30 09:03:08 PDT 2026
https://github.com/qiyao updated https://github.com/llvm/llvm-project/pull/213055
>From a950c8cb0ddad82702475aa89049bf8b8669e900 Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Sat, 20 Jun 2026 16:39:25 +0100
Subject: [PATCH 1/2] [lldb][DIL] Validate bitfield extraction ranges
The DIL bitfield extraction operator `base[high:low]` creates a
synthetic bitfield child without validating the requested range. Three
malformed ranges reach the data layer and either return nonsense or
crash. Reproduced with a 32-bit `int value` and DIL enabled:
```
(lldb) settings set target.experimental.use-DIL true
(lldb) frame variable 'value[-1:0]'
(int:2) value[-1:0] = 2
```
A negative index is accepted and produces a meaningless child.
`first_index`/`last_index` are signed `int64_t`, but
`GetSyntheticBitFieldChild` takes `uint32_t`, so `-1` silently wraps to
a huge unsigned offset.
```
(lldb) frame variable 'value[0:64]'
Assertion failed: (bitfield_bit_size <= 64), function GetMaxU64Bitfield,
file DataExtractor.cpp, line 580.
```
A width greater than 64 bits aborts. `DataExtractor::GetMaxU64Bitfield`
only supports up to 64 bits: it asserts in an assertions build and
otherwise performs an out-of-bounds shift. A 32-bit `value` with range
`[0:64]` is 65 bits, enough to trip it.
```
(lldb) frame variable 'value[100:50]'
(int:51) value[100:50] = 0
```
A high index past the base object's storage returns a garbage child in a
normal build. Under UBSan the read/format path shifts by an oversized
amount derived from the offset:
```
(lldb) frame variable 'value[100:50]'
DataExtractor.cpp:591:12: runtime error: shift exponent 234 is too large
for 64-bit type 'uint64_t'
```
Reject all three in the DIL evaluator before the synthetic child is
created: a negative `first_index`/`last_index`, a normalized width
greater than 64 bits, and a high index at or beyond the base object's
bit size (queried with `GetCompilerType().GetBitSize`). Each returns a
`DILDiagnosticError` with a clear message. Valid in-range extractions
are unaffected.
Adds the three malformed ranges to the DIL bitfield extraction API test
(`TestFrameVarDILBitFieldExtraction`). Without the fix the test fails on
the first case (`value[-1:0]` is expected to error but succeeds); the
`[0:64]` case additionally asserts and the `[100:50]` case is a UBSan
shift-out-of-bounds.
---
lldb/source/ValueObject/DILEval.cpp | 38 +++++++++++++++++++
.../TestFrameVarDILBitFieldExtraction.py | 34 +++++++++++++++++
2 files changed, 72 insertions(+)
diff --git a/lldb/source/ValueObject/DILEval.cpp b/lldb/source/ValueObject/DILEval.cpp
index 4c5ac96dccf74..7e443419bee2d 100644
--- a/lldb/source/ValueObject/DILEval.cpp
+++ b/lldb/source/ValueObject/DILEval.cpp
@@ -1277,14 +1277,52 @@ Interpreter::Visit(const BitFieldExtractionNode &node) {
return llvm::make_error<DILDiagnosticError>(
m_expr, "could not get the index as an integer", node.GetLocation());
+ if (first_index < 0 || last_index < 0) {
+ std::string message =
+ llvm::formatv("bitfield range {0}:{1} is not valid (negative index)",
+ first_index, last_index);
+ return llvm::make_error<DILDiagnosticError>(m_expr, message,
+ node.GetLocation());
+ }
+
// if the format given is [high-low], swap range
if (first_index > last_index)
std::swap(first_index, last_index);
+ // The underlying DataExtractor bitfield accessors only support extracting up
+ // to 64 bits at once (GetMaxU64Bitfield asserts bitfield_bit_size <= 64 and
+ // would otherwise perform an out-of-bounds shift). Reject oversized ranges
+ // here instead of crashing deep in the data layer.
+ if (last_index - first_index + 1 > 64) {
+ std::string message =
+ llvm::formatv("bitfield range {0}:{1} is not valid (more than 64 bits)",
+ first_index, last_index);
+ return llvm::make_error<DILDiagnosticError>(m_expr, message,
+ node.GetLocation());
+ }
+
auto base_or_err = EvaluateAndDereference(node.GetBase());
if (!base_or_err)
return base_or_err;
lldb::ValueObjectSP base = *base_or_err;
+
+ // The bitfield range must lie within the storage of the base object. A
+ // bit offset/size that extends past the base's bit size leads to an
+ // out-of-bounds shift when the value is later read or formatted (e.g. in
+ // DataExtractor::GetMaxU64Bitfield via DumpDataExtractor).
+ llvm::Expected<uint64_t> base_bit_size =
+ base->GetCompilerType().GetBitSize(&m_stack_frame);
+ if (!base_bit_size)
+ return base_bit_size.takeError();
+ if (static_cast<uint64_t>(last_index) >= *base_bit_size) {
+ std::string message = llvm::formatv(
+ "bitfield range {0}:{1} is not valid for \"({2}) {3}\"", first_index,
+ last_index, base->GetTypeName().AsCString("<invalid type>"),
+ base->GetName().GetStringRef());
+ return llvm::make_error<DILDiagnosticError>(m_expr, message,
+ node.GetLocation());
+ }
+
lldb::ValueObjectSP child_valobj_sp =
base->GetSyntheticBitFieldChild(first_index, last_index, true);
if (!child_valobj_sp) {
diff --git a/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py b/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py
index 026658c0cd25f..11dce1e494ead 100644
--- a/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py
+++ b/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py
@@ -66,3 +66,37 @@ def test_bitfield_extraction(self):
error=True,
substrs=["bit index is not an integer"],
)
+
+ # A negative bit index must be rejected with a clear error instead of
+ # silently wrapping to a huge uint32_t at the GetSyntheticBitFieldChild
+ # call site.
+ self.expect(
+ "frame var 'value[-1:0]'",
+ error=True,
+ substrs=["bitfield range -1:0 is not valid (negative index)"],
+ )
+ self.expect(
+ "frame var 'value[0:-1]'",
+ error=True,
+ substrs=["bitfield range 0:-1 is not valid (negative index)"],
+ )
+
+ # A bitfield wider than 64 bits must be rejected. The underlying
+ # DataExtractor::GetMaxU64Bitfield only supports up to 64 bits
+ # (it asserts and otherwise performs an out-of-bounds shift).
+ self.expect(
+ "frame var 'value[0:64]'",
+ error=True,
+ substrs=["bitfield range 0:64 is not valid (more than 64 bits)"],
+ )
+
+ # A bitfield whose high index is past the base object's storage must be
+ # rejected. Otherwise reading/formatting the synthetic child performs an
+ # out-of-bounds shift in DataExtractor::GetMaxU64Bitfield. 'value' is a
+ # 32-bit int, so bit index 50 is out of range. The range is normalized
+ # (high:low swapped) before the message is built.
+ self.expect(
+ "frame var 'value[100:50]'",
+ error=True,
+ substrs=["bitfield range 50:100 is not valid"],
+ )
>From 4dc78c1650ca4fc9a9ee5a05dc33cb06a76aca92 Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Thu, 30 Jul 2026 17:02:17 +0100
Subject: [PATCH 2/2] Simplify comments
---
lldb/source/ValueObject/DILEval.cpp | 17 ++++++++--------
.../TestFrameVarDILBitFieldExtraction.py | 20 +++++++++----------
2 files changed, 18 insertions(+), 19 deletions(-)
diff --git a/lldb/source/ValueObject/DILEval.cpp b/lldb/source/ValueObject/DILEval.cpp
index 7e443419bee2d..d2108cdca11a4 100644
--- a/lldb/source/ValueObject/DILEval.cpp
+++ b/lldb/source/ValueObject/DILEval.cpp
@@ -1277,6 +1277,9 @@ Interpreter::Visit(const BitFieldExtractionNode &node) {
return llvm::make_error<DILDiagnosticError>(
m_expr, "could not get the index as an integer", node.GetLocation());
+ // Reject negative indices before the swap below, so the diagnostic reports
+ // the range as the user wrote it. A negative index would also wrap to a huge
+ // offset in the uint32_t GetSyntheticBitFieldChild call below.
if (first_index < 0 || last_index < 0) {
std::string message =
llvm::formatv("bitfield range {0}:{1} is not valid (negative index)",
@@ -1289,10 +1292,9 @@ Interpreter::Visit(const BitFieldExtractionNode &node) {
if (first_index > last_index)
std::swap(first_index, last_index);
- // The underlying DataExtractor bitfield accessors only support extracting up
- // to 64 bits at once (GetMaxU64Bitfield asserts bitfield_bit_size <= 64 and
- // would otherwise perform an out-of-bounds shift). Reject oversized ranges
- // here instead of crashing deep in the data layer.
+ // GetMaxU64Bitfield in the data layer only supports up to 64 bits (it asserts
+ // bitfield_bit_size <= 64 and otherwise shifts out of bounds), so reject a
+ // wider range here.
if (last_index - first_index + 1 > 64) {
std::string message =
llvm::formatv("bitfield range {0}:{1} is not valid (more than 64 bits)",
@@ -1306,10 +1308,9 @@ Interpreter::Visit(const BitFieldExtractionNode &node) {
return base_or_err;
lldb::ValueObjectSP base = *base_or_err;
- // The bitfield range must lie within the storage of the base object. A
- // bit offset/size that extends past the base's bit size leads to an
- // out-of-bounds shift when the value is later read or formatted (e.g. in
- // DataExtractor::GetMaxU64Bitfield via DumpDataExtractor).
+ // The high index must lie within the base object's storage; a bit index past
+ // its bit size shifts out of bounds when the child is later read or formatted
+ // (GetMaxU64Bitfield).
llvm::Expected<uint64_t> base_bit_size =
base->GetCompilerType().GetBitSize(&m_stack_frame);
if (!base_bit_size)
diff --git a/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py b/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py
index 11dce1e494ead..088b753172e09 100644
--- a/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py
+++ b/lldb/test/API/commands/frame/var-dil/basics/BitFieldExtraction/TestFrameVarDILBitFieldExtraction.py
@@ -67,9 +67,8 @@ def test_bitfield_extraction(self):
substrs=["bit index is not an integer"],
)
- # A negative bit index must be rejected with a clear error instead of
- # silently wrapping to a huge uint32_t at the GetSyntheticBitFieldChild
- # call site.
+ # A negative bit index must be rejected instead of wrapping to a huge
+ # uint32_t at the GetSyntheticBitFieldChild call site.
self.expect(
"frame var 'value[-1:0]'",
error=True,
@@ -81,20 +80,19 @@ def test_bitfield_extraction(self):
substrs=["bitfield range 0:-1 is not valid (negative index)"],
)
- # A bitfield wider than 64 bits must be rejected. The underlying
- # DataExtractor::GetMaxU64Bitfield only supports up to 64 bits
- # (it asserts and otherwise performs an out-of-bounds shift).
+ # A range wider than 64 bits must be rejected: DataExtractor's
+ # GetMaxU64Bitfield only supports up to 64 bits (it asserts and
+ # otherwise shifts out of bounds).
self.expect(
"frame var 'value[0:64]'",
error=True,
substrs=["bitfield range 0:64 is not valid (more than 64 bits)"],
)
- # A bitfield whose high index is past the base object's storage must be
- # rejected. Otherwise reading/formatting the synthetic child performs an
- # out-of-bounds shift in DataExtractor::GetMaxU64Bitfield. 'value' is a
- # 32-bit int, so bit index 50 is out of range. The range is normalized
- # (high:low swapped) before the message is built.
+ # A high index past the base object's storage must be rejected;
+ # otherwise reading the synthetic child shifts out of bounds in
+ # GetMaxU64Bitfield. 'value' is a 32-bit int, so bit 50 is out of range.
+ # The range is normalized (high:low swapped) before the message.
self.expect(
"frame var 'value[100:50]'",
error=True,
More information about the lldb-commits
mailing list