Skip to content

Commit ef8fe91

Browse files
mergify[bot]woody-appleclaude
authored
ASN1Reader: reject child-overruns-parent and fix GetBitString shift UB (project-chip#71936) (project-chip#72335)
This narrows an earlier omnibus change to two defensible parts plus defense-in-depth hardening, and drops the test-only friend hook that was added to the public ASN1.h header. Behavior change (the substantive part): ASN1Reader::Next() and ExitContainer() previously let an inner element whose declared length overruns its parent container advance mElemStart past mContainerEnd; the next position check then returned ASN1_END, which callers and the ASN1_EXIT_* macros treat as a clean end of container -- silently swallowing the malformed encoding. These now return ASN1_ERROR_INVALID_ENCODING. A child ending exactly at mContainerEnd still reports ASN1_END, so well-formed DER (including Matter operational/DAC certs, which fit comfortably inside their containers) is unaffected. Proven by Next_BoundsExceededReturnsErrorNotEnd and DumpLikeLoop_RejectsBoundsOverrun, which build real DER blobs that return ASN1_END on master and INVALID_ENCODING after this change. Correctness fix: GetBitString() shifted an int-promoted uint8_t left by up to 24; a byte with bit 7 set produces signed-overflow UB (caught by -fsanitize=shift). The cast now happens before the shift. Proven by GetBitString_HighBitAtMaxShift_NoUndefinedBehavior and GetBitString_AllBytesHighBit_ExactPattern. Defense-in-depth (NOT a live bug): uint32 addition-overflow guards (mHeadLen + ValueLen) and mContainerEnd >= mElemStart consistency checks in Next()/ExitContainer()/ GetConstructedType()/Get*(), plus a peek-then-commit reordering of EnterContainer()/ExitContainer() so a failed precondition leaves reader state untouched. These are hardening only: DecodeHead already caps ValueLen at the remaining buffer, so mHeadLen + ValueLen cannot wrap a uint32 from any input reachable through the public API. No proving test fabricates the unreachable wrapped state, and no friend accessor is added. The earlier friend struct in ASN1.h and CHIP_CONFIG_TEST=1 in the test BUILD.gn have been removed; all tests now drive the reader through its public API only. (cherry picked from commit 22d69ed) Co-authored-by: Justin Wood <woody@apple.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4324ec9 commit ef8fe91

2 files changed

Lines changed: 603 additions & 13 deletions

File tree

src/lib/asn1/ASN1Reader.cpp

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,33 @@ CHIP_ERROR ASN1Reader::Next()
5353
VerifyOrReturnError(!EndOfContents, ASN1_END);
5454
VerifyOrReturnError(!IndefiniteLen, ASN1_ERROR_UNSUPPORTED_ENCODING);
5555

56-
// Note: avoid using addition assignment operator (+=), which may result in integer overflow
57-
// in the right hand side of an assignment (mHeadLen + ValueLen).
56+
// Defense-in-depth against integer overflow: mHeadLen + ValueLen are both uint32_t and
57+
// their sum could in principle wrap around, advancing mElemStart to an unintended
58+
// location. DecodeHead already caps ValueLen at the remaining buffer, so this cannot
59+
// wrap from any input reaching Next() through the public API; the guard hardens against
60+
// a future caller or a regression in those upstream bounds checks.
61+
VerifyOrReturnError(mHeadLen <= UINT32_MAX - ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
62+
// Defend against an inconsistent reader state where mElemStart has advanced past
63+
// mContainerEnd (e.g. a prior failed unwind left stale pointers). Without this
64+
// check the pointer subtraction below would underflow when cast to size_t and the
65+
// bounds comparison would silently pass.
66+
VerifyOrReturnError(mContainerEnd >= mElemStart, ASN1_ERROR_INVALID_STATE);
67+
// Compare in integer space rather than pointer space. On 32-bit targets size_t == uint32_t,
68+
// so the size_t cast provides no additional width; the preceding LENGTH_OVERFLOW guard is
69+
// what prevents wrap there. The size_t widening eliminates pointer-arithmetic UB on 64-bit
70+
// targets where computing `mElemStart + mHeadLen + ValueLen` could otherwise wrap past the
71+
// end of the address space before the comparison runs.
72+
//
73+
// BEHAVIOR CHANGE: a bounds-exceeded result here means a child element claims more bytes
74+
// than its parent container holds (malformed encoding). Previously this advanced mElemStart
75+
// past mContainerEnd and the subsequent `mElemStart != mContainerEnd` check returned ASN1_END,
76+
// which callers (and the ASN1_EXIT_* macros) treat as a clean end of container -- silently
77+
// swallowing the malformed input. It now surfaces as ASN1_ERROR_INVALID_ENCODING. A child
78+
// ending exactly at mContainerEnd still passes this `<=` check and reports ASN1_END as before,
79+
// so well-formed certificates are unaffected.
80+
VerifyOrReturnError(static_cast<size_t>(mHeadLen) + ValueLen <= static_cast<size_t>(mContainerEnd - mElemStart),
81+
ASN1_ERROR_INVALID_ENCODING);
82+
5883
mElemStart = mElemStart + mHeadLen + ValueLen;
5984

6085
ResetElementState();
@@ -80,6 +105,14 @@ CHIP_ERROR ASN1Reader::GetConstructedType(const uint8_t *& val, uint32_t & valLe
80105
{
81106
VerifyOrReturnError(Constructed, ASN1_ERROR_INVALID_STATE);
82107

108+
// Defend against an inconsistent reader state where mElemStart has advanced past
109+
// mContainerEnd. Required ahead of any size_t-widened subtraction so the result
110+
// cannot underflow.
111+
VerifyOrReturnError(mContainerEnd >= mElemStart, ASN1_ERROR_INVALID_STATE);
112+
// Guard against integer overflow: mHeadLen + ValueLen are both uint32_t and their sum
113+
// could wrap around, producing a bogus length for the caller.
114+
VerifyOrReturnError(mHeadLen <= UINT32_MAX - ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
115+
83116
val = mElemStart;
84117
valLen = mHeadLen + ValueLen;
85118

@@ -105,6 +138,17 @@ CHIP_ERROR ASN1Reader::EnterContainer(uint32_t offset)
105138
{
106139
VerifyOrReturnError(mNumSavedContexts != kMaxContextDepth, ASN1_ERROR_MAX_DEPTH_EXCEEDED);
107140

141+
// Peek-then-commit: validate all preconditions BEFORE mutating reader
142+
// state. If any guard fires the saved-context stack, mElemStart, and
143+
// mContainerEnd are all left untouched so the reader stays in a
144+
// consistent state.
145+
if (!IndefiniteLen)
146+
{
147+
VerifyOrReturnError(CanCastTo<uint32_t>(mBufEnd - Value), ASN1_ERROR_VALUE_OVERFLOW);
148+
VerifyOrReturnError(static_cast<uint32_t>(mBufEnd - Value) >= ValueLen, ASN1_ERROR_VALUE_OVERFLOW);
149+
}
150+
151+
// All checks passed - now commit by pushing the saved context and updating reader state.
108152
mSavedContexts[mNumSavedContexts].ElemStart = mElemStart;
109153
mSavedContexts[mNumSavedContexts].HeadLen = mHeadLen;
110154
mSavedContexts[mNumSavedContexts].ValueLen = ValueLen;
@@ -115,8 +159,6 @@ CHIP_ERROR ASN1Reader::EnterContainer(uint32_t offset)
115159
mElemStart = Value + offset;
116160
if (!IndefiniteLen)
117161
{
118-
VerifyOrReturnError(CanCastTo<uint32_t>(mBufEnd - Value), ASN1_ERROR_VALUE_OVERFLOW);
119-
VerifyOrReturnError(static_cast<uint32_t>(mBufEnd - Value) >= ValueLen, ASN1_ERROR_VALUE_OVERFLOW);
120162
mContainerEnd = Value + ValueLen;
121163
}
122164

@@ -129,10 +171,33 @@ CHIP_ERROR ASN1Reader::ExitContainer()
129171
{
130172
VerifyOrReturnError(mNumSavedContexts != 0, ASN1_ERROR_INVALID_STATE);
131173

132-
ASN1ParseContext & prevContext = mSavedContexts[--mNumSavedContexts];
174+
// Peek-then-commit: validate the saved context BEFORE mutating any reader
175+
// state. On any error the saved-context stack is left untouched so the
176+
// reader stays in a consistent state and a follow-up call sees the same
177+
// failure (rather than silently operating on a half-popped stack).
178+
const ASN1ParseContext & prevContext = mSavedContexts[mNumSavedContexts - 1];
133179

134180
VerifyOrReturnError(!prevContext.IndefiniteLen, ASN1_ERROR_UNSUPPORTED_ENCODING);
135181

182+
// Guard against integer overflow: HeadLen + ValueLen are both uint32_t and their sum
183+
// could wrap around, potentially advancing mElemStart to an unintended location.
184+
VerifyOrReturnError(prevContext.HeadLen <= UINT32_MAX - prevContext.ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
185+
// Defend against a saved context with ContainerEnd < ElemStart so the pointer
186+
// subtraction below cannot underflow when widened to size_t.
187+
VerifyOrReturnError(prevContext.ContainerEnd >= prevContext.ElemStart, ASN1_ERROR_INVALID_STATE);
188+
// Compare in integer space rather than pointer space. On 32-bit targets size_t == uint32_t
189+
// and the LENGTH_OVERFLOW guard above is what prevents wrap; the size_t widening eliminates
190+
// pointer-arithmetic UB on 64-bit targets where `prevContext.ElemStart + prevContext.HeadLen
191+
// + prevContext.ValueLen` could otherwise wrap past the end of the address space. A
192+
// bounds-exceeded result here means the saved container declared more bytes than its parent
193+
// holds (malformed encoding), NOT a clean end of container, so it must surface as
194+
// INVALID_ENCODING rather than ASN1_END.
195+
VerifyOrReturnError(static_cast<size_t>(prevContext.HeadLen) + prevContext.ValueLen <=
196+
static_cast<size_t>(prevContext.ContainerEnd - prevContext.ElemStart),
197+
ASN1_ERROR_INVALID_ENCODING);
198+
199+
// All checks passed - now commit by popping the saved context and updating reader state.
200+
--mNumSavedContexts;
136201
mElemStart = prevContext.ElemStart + prevContext.HeadLen + prevContext.ValueLen;
137202

138203
mContainerEnd = prevContext.ContainerEnd;
@@ -155,7 +220,15 @@ CHIP_ERROR ASN1Reader::GetInteger(int64_t & val)
155220
VerifyOrReturnError(Value != nullptr, ASN1_ERROR_INVALID_STATE);
156221
VerifyOrReturnError(ValueLen >= 1, ASN1_ERROR_INVALID_ENCODING);
157222
VerifyOrReturnError(ValueLen <= sizeof(int64_t), ASN1_ERROR_VALUE_OVERFLOW);
158-
VerifyOrReturnError(mElemStart + mHeadLen + ValueLen <= mContainerEnd, ASN1_ERROR_UNDERRUN);
223+
// Mirrors the guard sequence in Next(): (1) reject inconsistent state where
224+
// mContainerEnd < mElemStart (without this the size_t-widened subtraction below would
225+
// underflow to a huge value and the bounds compare would trivially pass), (2) reject
226+
// mHeadLen + ValueLen wrap on 32-bit targets where size_t == uint32_t, (3) bounds-check
227+
// via the size_t-widened comparison.
228+
VerifyOrReturnError(mContainerEnd >= mElemStart, ASN1_ERROR_INVALID_STATE);
229+
VerifyOrReturnError(mHeadLen <= UINT32_MAX - ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
230+
VerifyOrReturnError(static_cast<size_t>(mHeadLen) + ValueLen <= static_cast<size_t>(mContainerEnd - mElemStart),
231+
ASN1_ERROR_UNDERRUN);
159232

160233
if ((*Value & 0x80) == 0x80)
161234
{
@@ -175,7 +248,13 @@ CHIP_ERROR ASN1Reader::GetBoolean(bool & val)
175248
{
176249
VerifyOrReturnError(Value != nullptr, ASN1_ERROR_INVALID_STATE);
177250
VerifyOrReturnError(ValueLen == 1, ASN1_ERROR_INVALID_ENCODING);
178-
VerifyOrReturnError(mElemStart + mHeadLen + ValueLen <= mContainerEnd, ASN1_ERROR_UNDERRUN);
251+
// Mirrors the guard sequence in Next(): (1) reject inconsistent state where
252+
// mContainerEnd < mElemStart, (2) reject mHeadLen + ValueLen wrap on 32-bit
253+
// (size_t == uint32_t), (3) bounds-check via size_t-widened comparison.
254+
VerifyOrReturnError(mContainerEnd >= mElemStart, ASN1_ERROR_INVALID_STATE);
255+
VerifyOrReturnError(mHeadLen <= UINT32_MAX - ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
256+
VerifyOrReturnError(static_cast<size_t>(mHeadLen) + ValueLen <= static_cast<size_t>(mContainerEnd - mElemStart),
257+
ASN1_ERROR_UNDERRUN);
179258
VerifyOrReturnError(Value[0] == 0 || Value[0] == 0xFF, ASN1_ERROR_INVALID_ENCODING);
180259

181260
val = (Value[0] != 0);
@@ -188,7 +267,13 @@ CHIP_ERROR ASN1Reader::GetUTCTime(ASN1UniversalTime & outTime)
188267
// Supported Encoding: YYMMDDHHMMSSZ
189268
VerifyOrReturnError(Value != nullptr, ASN1_ERROR_INVALID_STATE);
190269
VerifyOrReturnError(ValueLen >= 1, ASN1_ERROR_INVALID_ENCODING);
191-
VerifyOrReturnError(mElemStart + mHeadLen + ValueLen <= mContainerEnd, ASN1_ERROR_UNDERRUN);
270+
// Mirrors the guard sequence in Next(): (1) reject inconsistent state where
271+
// mContainerEnd < mElemStart, (2) reject mHeadLen + ValueLen wrap on 32-bit
272+
// (size_t == uint32_t), (3) bounds-check via size_t-widened comparison.
273+
VerifyOrReturnError(mContainerEnd >= mElemStart, ASN1_ERROR_INVALID_STATE);
274+
VerifyOrReturnError(mHeadLen <= UINT32_MAX - ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
275+
VerifyOrReturnError(static_cast<size_t>(mHeadLen) + ValueLen <= static_cast<size_t>(mContainerEnd - mElemStart),
276+
ASN1_ERROR_UNDERRUN);
192277
VerifyOrReturnError(ValueLen == 13 && Value[12] == 'Z', ASN1_ERROR_UNSUPPORTED_ENCODING);
193278

194279
return outTime.ImportFrom_ASN1_TIME_string(CharSpan(reinterpret_cast<const char *>(Value), ValueLen));
@@ -199,7 +284,13 @@ CHIP_ERROR ASN1Reader::GetGeneralizedTime(ASN1UniversalTime & outTime)
199284
// Supported Encoding: YYYYMMDDHHMMSSZ
200285
VerifyOrReturnError(Value != nullptr, ASN1_ERROR_INVALID_STATE);
201286
VerifyOrReturnError(ValueLen >= 1, ASN1_ERROR_INVALID_ENCODING);
202-
VerifyOrReturnError(mElemStart + mHeadLen + ValueLen <= mContainerEnd, ASN1_ERROR_UNDERRUN);
287+
// Mirrors the guard sequence in Next(): (1) reject inconsistent state where
288+
// mContainerEnd < mElemStart, (2) reject mHeadLen + ValueLen wrap on 32-bit
289+
// (size_t == uint32_t), (3) bounds-check via size_t-widened comparison.
290+
VerifyOrReturnError(mContainerEnd >= mElemStart, ASN1_ERROR_INVALID_STATE);
291+
VerifyOrReturnError(mHeadLen <= UINT32_MAX - ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
292+
VerifyOrReturnError(static_cast<size_t>(mHeadLen) + ValueLen <= static_cast<size_t>(mContainerEnd - mElemStart),
293+
ASN1_ERROR_UNDERRUN);
203294
VerifyOrReturnError(ValueLen == 15 && Value[14] == 'Z', ASN1_ERROR_UNSUPPORTED_ENCODING);
204295

205296
return outTime.ImportFrom_ASN1_TIME_string(CharSpan(reinterpret_cast<const char *>(Value), ValueLen));
@@ -222,19 +313,28 @@ CHIP_ERROR ASN1Reader::GetBitString(uint32_t & outVal)
222313
VerifyOrReturnError(Value != nullptr, ASN1_ERROR_INVALID_STATE);
223314
VerifyOrReturnError(ValueLen >= 1, ASN1_ERROR_INVALID_ENCODING);
224315
VerifyOrReturnError(ValueLen <= 5, ASN1_ERROR_UNSUPPORTED_ENCODING);
225-
VerifyOrReturnError(mElemStart + mHeadLen + ValueLen <= mContainerEnd, ASN1_ERROR_UNDERRUN);
316+
// Mirrors the guard sequence in Next(): (1) reject inconsistent state where
317+
// mContainerEnd < mElemStart, (2) reject mHeadLen + ValueLen wrap on 32-bit
318+
// (size_t == uint32_t), (3) bounds-check via size_t-widened comparison.
319+
VerifyOrReturnError(mContainerEnd >= mElemStart, ASN1_ERROR_INVALID_STATE);
320+
VerifyOrReturnError(mHeadLen <= UINT32_MAX - ValueLen, ASN1_ERROR_LENGTH_OVERFLOW);
321+
VerifyOrReturnError(static_cast<size_t>(mHeadLen) + ValueLen <= static_cast<size_t>(mContainerEnd - mElemStart),
322+
ASN1_ERROR_UNDERRUN);
226323

227324
if (ValueLen == 1)
228325
{
229326
outVal = 0;
230327
}
231328
else
232329
{
233-
outVal = ReverseBits(Value[1]);
234-
int shift = 8;
330+
outVal = ReverseBits(Value[1]);
331+
unsigned int shift = 8;
235332
for (uint32_t i = 2; i < ValueLen; i++, shift += 8)
236333
{
237-
outVal |= static_cast<uint32_t>(ReverseBits(Value[i]) << shift);
334+
// Cast to uint32_t before shifting: ReverseBits returns uint8_t which
335+
// would be promoted to (signed) int, and shifts of 24+ on a value with
336+
// the high bit set are undefined behavior on signed integers.
337+
outVal |= (static_cast<uint32_t>(ReverseBits(Value[i])) << shift);
238338
}
239339
}
240340

0 commit comments

Comments
 (0)