[BUG] [Crash Matter >=1.5] Process Abort on Unexpected Message During Chunked List Write
Summary of Issue
This issue is only affecting Matter 1.5 and later. It occurs when a Matter device is processing a chunked write to a list attribute (e.g., AttributeList, AcceptedCommandList, or GeneratedCommandList). The first request leaves the WriteHandler in an active list-write state. If a subsequent unexpected message arrives on the same exchange (i.e., a non-WriteRequest message), the exchange may be closed before the handler finishes cleaning up its state.
During cleanup, WriteHandler::Close() still attempts to retrieve the accessing FabricIndex from the already-closed exchange. This causes ExchangeHolder::operator->() to trigger VerifyOrDie, resulting in a process abort.
Earlier Matter versions were not affected because the list-write cleanup path did not need to access the ExchangeContext. Before Matter 1.5, DeliverListWriteEnd() only notified the data model that the list write had completed, without retrieving the accessing FabricIndex. Matter 1.5 added GetAccessingFabricIndex() as a new argument to ListAttributeWriteNotification(), which introduced a new dereference of mExchangeCtx during cleanup. The underlying exchange-closing behavior already existed, but it was harmless because the cleanup path did not touch the exchange after it had been closed. The 1.5 change therefore exposed a previously latent lifetime mismatch between WriteHandler and ExchangeContext.
The longer explaination. While the report is organized by AI, the issue is validated on a development board.
Call Flow and Root Cause Analysis
The failure requires two messages on the same Matter exchange. The first message establishes an active chunked list-write transaction. The second, unexpected message causes the underlying exchange to close before WriteHandler has finished cleaning up the first transaction.
The resulting call flow is described below.
Step 1: A session/exchange receives a chunked WriteRequest
A Matter session is already established, and an ExchangeContext receives a WriteRequest.
The request targets a list attribute and sets:
MoreChunkedMessages = trueBecause additional chunks are expected, the WriteHandler remains active after processing this request instead of completing the transaction.
While processing the attribute, WriteHandler records that a list write is in progress:
mProcessingAttributePath = ...;
kProcessingAttributeIsList = true;Importantly, this state is established before the actual write authorization check. Therefore, even if the write is later rejected by ACL or because the attribute is not writable, the handler can still remain in the list-write state.
At this point:
Session alive
ExchangeContext alive
WriteHandler alive
List-write state active
More chunks expected yesStep 2: Another message arrives on the same exchange
Because MoreChunkedMessages was set, the same WriteHandler is still associated with the in-progress transaction.
Normally, the next message should be another WriteRequest.
Instead, a non-WriteRequest message arrives on the same exchange.
WriteHandler::OnMessageReceived() detects the unexpected message type and enters its error-handling path:
StatusResponse::Send(
Status::InvalidAction,
apExchangeContext,
false /* aExpectResponse */);
Close();The important detail is:
aExpectResponse = falseThis means that the generated StatusResponse does not expect another response from the peer.
Step 3: StatusResponse::Send() causes the exchange to close
Sending the StatusResponse eventually reaches the exchange-layer message handling logic.
Because:
no response is expected
+
no send remains outstandingthe exchange is considered complete.
As a result, the ExchangeContext is closed synchronously during StatusResponse::Send().
Conceptually, the call flow is:
WriteHandler::OnMessageReceived()
|
+-- StatusResponse::Send(..., false)
|
+-- ExchangeContext::SendMessage()
|
+-- MessageHandled()
|
+-- ExchangeContext::Close()This is the critical lifecycle transition: although WriteHandler::OnMessageReceived() has not yet called its own Close(), the underlying exchange has already been closed.
Step 4: ExchangeHolder is notified that the exchange is closing
WriteHandler does not hold the ExchangeContext as an unconditional raw pointer. It accesses it through an ExchangeHolder.
When the exchange closes, ExchangeHolder::OnExchangeClosing() is invoked and the holder drops its reference to the exchange.
Conceptually:
ExchangeHolder::OnExchangeClosing(...)
{
mpExchangeCtx = nullptr;
}Therefore, after StatusResponse::Send() returns, the state has changed to:
Session still exists
ExchangeContext closed
WriteHandler still alive
mExchangeCtx empty
List-write state still activeThis is the inconsistent state that leads to the abort.
Step 5: WriteHandler::OnMessageReceived() continues and calls Close()
Control returns from StatusResponse::Send() to WriteHandler::OnMessageReceived().
The next statement is:
Close();Therefore:
StatusResponse::Send(...)
|
+-- exchange closes
|
+-- mExchangeCtx becomes empty
|
v
WriteHandler::Close()WriteHandler::Close() is written under the assumption that its exchange is still available until it explicitly releases it:
void WriteHandler::Close()
{
VerifyOrReturn(mState != State::Uninitialized);
DeliverFinalListWriteEnd(false /* wasSuccessful */);
mExchangeCtx.Release();
...
}Under normal call paths, this ordering is valid:
Deliver final notification
↓
Release exchangeHowever, on the unexpected-message path, the real ordering has already become:
Exchange closed externally
↓
WriteHandler::Close()
↓
Deliver final notificationThus, DeliverFinalListWriteEnd() is executed after the exchange has already disappeared.
Step 6: Cleanup detects that a list write is still active
Because the first WriteRequest established an active list-write state, DeliverFinalListWriteEnd() does not return immediately.
The relevant condition is effectively:
if (mProcessingAttributePath.HasValue() &&
kProcessingAttributeIsList)
{
DeliverListWriteEnd(...);
}Therefore, WriteHandler::Close() attempts to deliver the final list-write notification for the interrupted transaction.
The call chain becomes:
WriteHandler::Close()
|
+-- DeliverFinalListWriteEnd()
|
+-- DeliverListWriteEnd()Step 7: DeliverListWriteEnd() requests the accessing FabricIndex
In affected versions, DeliverListWriteEnd() passes the accessing fabric index to ListAttributeWriteNotification().
Conceptually:
mDataModelProvider->ListAttributeWriteNotification(
aPath,
operation,
GetAccessingFabricIndex());This introduces another access to the exchange during cleanup.
The call chain now becomes:
WriteHandler::Close()
|
+-- DeliverFinalListWriteEnd()
|
+-- DeliverListWriteEnd()
|
+-- GetAccessingFabricIndex()Step 8: GetAccessingFabricIndex() dereferences the already-empty ExchangeHolder
GetAccessingFabricIndex() retrieves the fabric information through mExchangeCtx:
FabricIndex WriteHandler::GetAccessingFabricIndex() const
{
return mExchangeCtx->GetSessionHandle()->GetFabricIndex();
}However, mExchangeCtx no longer contains a valid ExchangeContext, because the exchange was already closed in Step 3.
Dereferencing the holder invokes ExchangeHolder::operator->(), which contains a validity assertion equivalent to:
VerifyOrDie(mpExchangeCtx != nullptr);At this point:
mpExchangeCtx == nullptrand therefore:
VerifyOrDie
↓
process abortThe observed failure is:
VerifyOrDie failure at src/messaging/ExchangeHolder.h:
mpExchangeCtx != nullptr
exit 134Complete Failure Sequence
The complete sequence can therefore be summarized as:
CASE Session / ExchangeContext
|
| Chunked WriteRequest
| List attribute
| MoreChunkedMessages = true
v
WriteHandler remains active
|
| list-write state is recorded
|
v
Unexpected non-WriteRequest message
on the SAME Exchange
|
v
WriteHandler::OnMessageReceived()
|
v
StatusResponse::Send(InvalidAction, ..., false)
|
v
ExchangeContext::MessageHandled()
|
v
ExchangeContext::Close()
|
v
ExchangeHolder::OnExchangeClosing()
|
v
mExchangeCtx becomes empty
|
| return to WriteHandler::OnMessageReceived()
v
WriteHandler::Close()
|
v
DeliverFinalListWriteEnd()
|
v
DeliverListWriteEnd()
|
v
GetAccessingFabricIndex()
|
v
mExchangeCtx->...
|
v
ExchangeHolder::operator->()
|
v
VerifyOrDie(mpExchangeCtx != nullptr)
|
v
PROCESS ABORTBug prevalence
always
GitHub hash of the SDK that was being used
e054bb8ac6
Platform
linux
Platform Version(s)
1.5, 1.6
Anything else?
No response
Source: project-chip/connectedhomeip