Skip to content

Commit c56eb2b

Browse files
andy31415bzbarsky-appleandreilitvin
authored
Validate for a valid cluster path when Invoke is called (#37207)
* Fix invoke verification of data existence of cluster and endpoint * Add unit test for codegen logic issue * Add integration test for return codes * Restule * Remove odd comment * Remove unused import * Use asserts fail * Add unit test that checks invalid cluster return code as well * Fix comment * Code review feedback: listing of commands should fail if the cluster does not exist * Update src/data-model-providers/codegen/CodegenDataModelProvider.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/data-model-providers/codegen/CodegenDataModelProvider.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/data-model-providers/codegen/CodegenDataModelProvider.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Updated comment * Try to reduce amount of code overhead for extra validation check * Update logic yet again since just checking for null fails unit tests and having unit tests seems reasonable * Restyle * Update src/data-model-providers/codegen/EmberMetadata.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/data-model-providers/codegen/EmberMetadata.h Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update src/data-model-providers/codegen/CodegenDataModelProvider.cpp Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> * Update comment * Update comment * Update based on code review ... if we cannot enforce an interface, we document. Not happy and hoping we can improve in the future --------- Co-authored-by: Boris Zbarsky <bzbarsky@apple.com> Co-authored-by: Andrei Litvin <andreilitvin@google.com>
1 parent 26341b4 commit c56eb2b

File tree

6 files changed

+218
-13
lines changed

6 files changed

+218
-13
lines changed

src/app/data-model-provider/Provider.h

+10
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ class Provider : public ProviderMetadataTree
8585
/// This includes cases where command handling and value return will be done asynchronously.
8686
/// - returning a value other than Success implies an error reply (error and data are mutually exclusive)
8787
///
88+
/// Preconditions:
89+
/// - `request.path` MUST be valid: Invoke` is only guaranteed to function correctly for
90+
/// VALID paths (i.e. use `ProviderMetadataTree::AcceptedCommands` to check). This is
91+
/// because we assume ACL or flags (like timed invoke) have to happen before invoking
92+
/// this command.
93+
/// - TODO: as interfaces are updated, we may want to make the above requirement more
94+
/// relaxed, as it seems desirable for users of this interface to have guaranteed
95+
/// behavior (like error on invalid paths) where as today this seems unclear as some
96+
/// command intercepts do not validate if the path is valid per endpoints.
97+
///
8898
/// Return value expectations:
8999
/// - if a response has been placed into `handler` then std::nullopt MUST be returned. In particular
90100
/// note that CHIP_NO_ERROR is NOT the same as std::nullopt:

src/data-model-providers/codegen/CodegenDataModelProvider.cpp

+13-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
#include "data-model-providers/codegen/EmberMetadata.h"
1718
#include <data-model-providers/codegen/CodegenDataModelProvider.h>
1819

1920
#include <access/AccessControl.h>
@@ -324,6 +325,12 @@ const EmberAfCluster * CodegenDataModelProvider::FindServerCluster(const Concret
324325
CHIP_ERROR CodegenDataModelProvider::AcceptedCommands(const ConcreteClusterPath & path,
325326
DataModel::ListBuilder<DataModel::AcceptedCommandEntry> & builder)
326327
{
328+
// Some CommandHandlerInterface instances are registered of ALL endpoints, so make sure first that
329+
// the cluster actually exists on this endpoint before asking the CommandHandlerInterface whether it
330+
// supports the command.
331+
const EmberAfCluster * serverCluster = FindServerCluster(path);
332+
VerifyOrReturnError(serverCluster != nullptr, CHIP_ERROR_NOT_FOUND);
333+
327334
CommandHandlerInterface * interface =
328335
CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(path.mEndpointId, path.mClusterId);
329336
if (interface != nullptr)
@@ -377,8 +384,6 @@ CHIP_ERROR CodegenDataModelProvider::AcceptedCommands(const ConcreteClusterPath
377384
VerifyOrReturnError(err == CHIP_ERROR_NOT_IMPLEMENTED, err);
378385
}
379386

380-
const EmberAfCluster * serverCluster = FindServerCluster(path);
381-
VerifyOrReturnError(serverCluster != nullptr, CHIP_ERROR_NOT_FOUND);
382387
VerifyOrReturnError(serverCluster->acceptedCommandList != nullptr, CHIP_NO_ERROR);
383388

384389
const chip::CommandId * endOfList = serverCluster->acceptedCommandList;
@@ -404,6 +409,12 @@ CHIP_ERROR CodegenDataModelProvider::AcceptedCommands(const ConcreteClusterPath
404409
CHIP_ERROR CodegenDataModelProvider::GeneratedCommands(const ConcreteClusterPath & path,
405410
DataModel::ListBuilder<CommandId> & builder)
406411
{
412+
// Some CommandHandlerInterface instances are registered of ALL endpoints, so make sure first that
413+
// the cluster actually exists on this endpoint before asking the CommandHandlerInterface whether it
414+
// supports the command.
415+
const EmberAfCluster * serverCluster = FindServerCluster(path);
416+
VerifyOrReturnError(serverCluster != nullptr, CHIP_ERROR_NOT_FOUND);
417+
407418
CommandHandlerInterface * interface =
408419
CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(path.mEndpointId, path.mClusterId);
409420
if (interface != nullptr)
@@ -454,8 +465,6 @@ CHIP_ERROR CodegenDataModelProvider::GeneratedCommands(const ConcreteClusterPath
454465
VerifyOrReturnError(err == CHIP_ERROR_NOT_IMPLEMENTED, err);
455466
}
456467

457-
const EmberAfCluster * serverCluster = FindServerCluster(path);
458-
VerifyOrReturnError(serverCluster != nullptr, CHIP_ERROR_NOT_FOUND);
459468
VerifyOrReturnError(serverCluster->generatedCommandList != nullptr, CHIP_NO_ERROR);
460469

461470
const chip::CommandId * endOfList = serverCluster->generatedCommandList;

src/data-model-providers/codegen/EmberMetadata.cpp

+21-8
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,11 @@ FindAttributeMetadata(const ConcreteAttributePath & aPath)
5656

5757
if (metadata == nullptr)
5858
{
59-
const EmberAfEndpointType * type = emberAfFindEndpointType(aPath.mEndpointId);
60-
if (type == nullptr)
61-
{
62-
return Status::UnsupportedEndpoint;
63-
}
59+
Status status = ValidateClusterPath(aPath);
6460

65-
const EmberAfCluster * cluster = emberAfFindClusterInType(type, aPath.mClusterId, CLUSTER_MASK_SERVER);
66-
if (cluster == nullptr)
61+
if (status != Status::Success)
6762
{
68-
return Status::UnsupportedCluster;
63+
return status;
6964
}
7065

7166
// Since we know the attribute is unsupported and the endpoint/cluster are
@@ -76,6 +71,24 @@ FindAttributeMetadata(const ConcreteAttributePath & aPath)
7671
return metadata;
7772
}
7873

74+
Status ValidateClusterPath(const ConcreteClusterPath & path)
75+
{
76+
77+
const EmberAfEndpointType * type = emberAfFindEndpointType(path.mEndpointId);
78+
if (type == nullptr)
79+
{
80+
return Status::UnsupportedEndpoint;
81+
}
82+
83+
const EmberAfCluster * cluster = emberAfFindClusterInType(type, path.mClusterId, CLUSTER_MASK_SERVER);
84+
if (cluster == nullptr)
85+
{
86+
return Status::UnsupportedCluster;
87+
}
88+
89+
return Status::Success;
90+
}
91+
7992
} // namespace Ember
8093
} // namespace app
8194
} // namespace chip

src/data-model-providers/codegen/EmberMetadata.h

+9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
#pragma once
1818

19+
#include "app/ConcreteClusterPath.h"
1920
#include <app/util/af-types.h>
2021
#include <lib/core/CHIPError.h>
2122
#include <protocols/interaction_model/StatusCode.h>
@@ -42,6 +43,14 @@ std::variant<const EmberAfCluster *, // global attribute, data from
4243
>
4344
FindAttributeMetadata(const ConcreteAttributePath & aPath);
4445

46+
/// Returns the status for a given cluster existing in the ember metadata.
47+
///
48+
/// Return code will be one of:
49+
/// - Status::UnsupportedEndpoint if the path endpoint does not exist
50+
/// - Status::UnsupportedCluster if the cluster does not exist on the given endpoint
51+
/// - Status::Success if the cluster exists in the ember metadata.
52+
Protocols::InteractionModel::Status ValidateClusterPath(const ConcreteClusterPath & path);
53+
4554
} // namespace Ember
4655
} // namespace app
4756
} // namespace chip

src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp

+84-1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,44 @@ class MockAccessControl : public Access::AccessControl::Delegate, public Access:
218218
bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint) override { return true; }
219219
};
220220

221+
class MockCommandHandler : public CommandHandler
222+
{
223+
public:
224+
CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath,
225+
const Protocols::InteractionModel::ClusterStatusCode & aStatus,
226+
const char * context = nullptr) override
227+
{
228+
// MOCK: do not do anything here
229+
return CHIP_NO_ERROR;
230+
}
231+
232+
void AddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::ClusterStatusCode & aStatus,
233+
const char * context = nullptr) override
234+
{
235+
// MOCK: do not do anything here
236+
}
237+
238+
FabricIndex GetAccessingFabricIndex() const override { return 1; }
239+
240+
CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId,
241+
const DataModel::EncodableToTLV & aEncodable) override
242+
{
243+
return CHIP_NO_ERROR;
244+
}
245+
246+
void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId,
247+
const DataModel::EncodableToTLV & aEncodable) override
248+
{}
249+
250+
bool IsTimedInvoke() const override { return false; }
251+
252+
void FlushAcksRightAwayOnSlowCommand() override {}
253+
254+
Access::SubjectDescriptor GetSubjectDescriptor() const override { return kAdminSubjectDescriptor; }
255+
256+
Messaging::ExchangeContext * GetExchangeContext() const override { return nullptr; }
257+
};
258+
221259
/// Overrides Enumerate*Commands in the CommandHandlerInterface to allow
222260
/// testing of behaviors when command enumeration is done in the interace.
223261
class CustomListCommandHandler : public CommandHandlerInterface
@@ -229,7 +267,20 @@ class CustomListCommandHandler : public CommandHandlerInterface
229267
}
230268
~CustomListCommandHandler() { CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this); }
231269

232-
void InvokeCommand(HandlerContext & handlerContext) override { handlerContext.SetCommandNotHandled(); }
270+
void InvokeCommand(HandlerContext & handlerContext) override
271+
{
272+
if (mHandleCommand)
273+
{
274+
handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Protocols::InteractionModel::Status::Success);
275+
handlerContext.SetCommandHandled();
276+
}
277+
else
278+
{
279+
handlerContext.SetCommandNotHandled();
280+
}
281+
}
282+
283+
void SetHandleCommands(bool handle) { mHandleCommand = handle; }
233284

234285
CHIP_ERROR EnumerateAcceptedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) override
235286
{
@@ -268,6 +319,7 @@ class CustomListCommandHandler : public CommandHandlerInterface
268319
private:
269320
bool mOverrideAccepted = false;
270321
bool mOverrideGenerated = false;
322+
bool mHandleCommand = false;
271323

272324
std::vector<CommandId> mAccepted;
273325
std::vector<CommandId> mGenerated;
@@ -1156,6 +1208,37 @@ TEST_F(TestCodegenModelViaMocks, IterateOverGeneratedCommands)
11561208
ASSERT_TRUE(cmds.data_equal(Span<const CommandId>(expectedCommands3)));
11571209
}
11581210

1211+
TEST_F(TestCodegenModelViaMocks, AcceptedGeneratedCommandsOnInvalidEndpoints)
1212+
{
1213+
UseMockNodeConfig config(gTestNodeConfig);
1214+
CodegenDataModelProviderWithContext model;
1215+
1216+
// register a CHI on ALL endpoints
1217+
CustomListCommandHandler handler(chip::NullOptional, MockClusterId(1));
1218+
handler.SetHandleCommands(true);
1219+
1220+
DataModel::ListBuilder<CommandId> generatedBuilder;
1221+
DataModel::ListBuilder<DataModel::AcceptedCommandEntry> acceptedBuilder;
1222+
1223+
// valid endpoint will result in valid data (even though list is empty)
1224+
ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), generatedBuilder), CHIP_NO_ERROR);
1225+
ASSERT_TRUE(generatedBuilder.IsEmpty());
1226+
ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), acceptedBuilder), CHIP_NO_ERROR);
1227+
ASSERT_TRUE(acceptedBuilder.IsEmpty());
1228+
1229+
// Invalid endpoint fails - we will get no commands there (even though CHI is registered)
1230+
ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), generatedBuilder),
1231+
CHIP_ERROR_NOT_FOUND);
1232+
ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), acceptedBuilder),
1233+
CHIP_ERROR_NOT_FOUND);
1234+
1235+
// same for invalid cluster ID
1236+
ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(0x1123)), generatedBuilder),
1237+
CHIP_ERROR_NOT_FOUND);
1238+
ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(0x1123)), acceptedBuilder),
1239+
CHIP_ERROR_NOT_FOUND);
1240+
}
1241+
11591242
TEST_F(TestCodegenModelViaMocks, CommandHandlerInterfaceCommandHandling)
11601243
{
11611244

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#
2+
# Copyright (c) 2025 Project CHIP Authors
3+
# All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# === BEGIN CI TEST ARGUMENTS ===
19+
# test-runner-runs:
20+
# run1:
21+
# app: ${ALL_CLUSTERS_APP}
22+
# app-args: >
23+
# --discriminator 1234
24+
# --KVS kvs1
25+
# --trace-to json:${TRACE_APP}.json
26+
# script-args: >
27+
# --storage-path admin_storage.json
28+
# --commissioning-method on-network
29+
# --discriminator 1234
30+
# --passcode 20202021
31+
# --trace-to json:${TRACE_TEST_JSON}.json
32+
# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
33+
# factory-reset: true
34+
# quiet: true
35+
# === END CI TEST ARGUMENTS ===
36+
37+
import chip.clusters as Clusters
38+
from chip.interaction_model import InteractionModelError, Status
39+
from chip.testing.matter_testing import MatterBaseTest, async_test_body, default_matter_test_main
40+
from mobly import asserts
41+
42+
43+
class TestInvokeReturnCodes(MatterBaseTest):
44+
"""
45+
Validates that the invoke action correctly refuses commands
46+
on invalid endpoints.
47+
"""
48+
49+
@async_test_body
50+
async def test_invalid_endpoint_command(self):
51+
self.print_step(0, "Commissioning - already done")
52+
53+
self.print_step(1, "Find an invalid endpoint id")
54+
root_parts = await self.read_single_attribute_check_success(
55+
cluster=Clusters.Descriptor,
56+
attribute=Clusters.Descriptor.Attributes.PartsList,
57+
endpoint=0,
58+
)
59+
endpoints = set(root_parts)
60+
invalid_endpoint_id = 1
61+
while invalid_endpoint_id in endpoints:
62+
invalid_endpoint_id += 1
63+
64+
self.print_step(
65+
2,
66+
"Attempt to invoke SoftwareDiagnostics::ResetWatermarks on an invalid endpoint",
67+
)
68+
try:
69+
await self.send_single_cmd(
70+
cmd=Clusters.SoftwareDiagnostics.Commands.ResetWatermarks(),
71+
endpoint=invalid_endpoint_id,
72+
)
73+
asserts.fail("Unexpected command success on an invalid endpoint")
74+
except InteractionModelError as e:
75+
asserts.assert_equal(
76+
e.status, Status.UnsupportedEndpoint, "Unexpected error returned"
77+
)
78+
79+
80+
if __name__ == "__main__":
81+
default_matter_test_main()

0 commit comments

Comments
 (0)