Skip to content

Commit

Permalink
Add some machinery for structural validation in MTRDevice_XPC and MTR…
Browse files Browse the repository at this point in the history
…Device_Concrete. (#36224)

* Add some machinery for structural validation in MTRDevice_XPC and MTRDevice_Concrete.

Validate things that get injected (that we got from the XPC transport) as well
as various bits in MTRDevice_XPC.

* Address review comments.

* Update src/darwin/Framework/CHIP/MTRDeviceDataValidation.mm

---------

Co-authored-by: Justin Wood <[email protected]>
  • Loading branch information
bzbarsky-apple and woody-apple authored Oct 25, 2024
1 parent dc52d9d commit b7aa537
Show file tree
Hide file tree
Showing 15 changed files with 729 additions and 52 deletions.
1 change: 1 addition & 0 deletions src/darwin/Framework/CHIP/MTRBaseDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#import "MTRCluster.h"
#import "MTRClusterStateCacheContainer_Internal.h"
#import "MTRCluster_Internal.h"
#import "MTRDeviceDataValidation.h"
#import "MTRDevice_Internal.h"
#import "MTRError_Internal.h"
#import "MTREventTLVValueDecoder_Internal.h"
Expand Down
2 changes: 1 addition & 1 deletion src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#import "MTRBaseDevice.h"
#import <Foundation/Foundation.h>

#import "MTRDeviceDataValueDictionary.h"
#import "MTRDefines_Internal.h"

#include <app/AttributePathParams.h>
#include <app/ConcreteAttributePath.h>
Expand Down
17 changes: 17 additions & 0 deletions src/darwin/Framework/CHIP/MTRDefines_Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ typedef struct {} variable_hidden_by_mtr_hide;
// Default timed interaction timeout, in ms, if another one is not provided.
#define MTR_DEFAULT_TIMED_INTERACTION_TIMEOUT_MS 10000

// Useful building block for type-checking machinery. Uses C-style cast so it
// can be used in .m files as well.
#define MTR_SAFE_CAST(object, classname) \
([object isKindOfClass:[classname class]] ? (classname *) (object) : nil)

#pragma mark - XPC Defines

#define MTR_SIMPLE_REMOTE_XPC_GETTER(XPC_CONNECTION, NAME, TYPE, DEFAULT_VALUE, GETTER_NAME, PREFIX) \
Expand Down Expand Up @@ -179,3 +184,15 @@ typedef struct {} variable_hidden_by_mtr_hide;

#define MTR_ABSTRACT_METHOD() \
_MTR_ABSTRACT_METHOD_IMPL("%@ or some ancestor must implement %@", self.class, NSStringFromSelector(_cmd))

#pragma mark - Typedefs for some commonly used types.

/**
* A data-value as defined in MTRBaseDevice.h.
*/
typedef NSDictionary<NSString *, id> * MTRDeviceDataValueDictionary;

/**
* A response-value as defined in MTRBaseDevice.h.
*/
typedef NSDictionary<NSString *, id> * MTRDeviceResponseValueDictionary;
1 change: 0 additions & 1 deletion src/darwin/Framework/CHIP/MTRDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
#import "MTRConversion.h"
#import "MTRDefines_Internal.h"
#import "MTRDeviceController_Internal.h"
#import "MTRDeviceDataValueDictionary.h"
#import "MTRDevice_Internal.h"
#import "MTRError_Internal.h"
#import "MTRLogging_Internal.h"
Expand Down
1 change: 0 additions & 1 deletion src/darwin/Framework/CHIP/MTRDeviceClusterData.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
#import <Foundation/Foundation.h>

#import "MTRDefines_Internal.h"
#import "MTRDeviceDataValueDictionary.h"

NS_ASSUME_NONNULL_BEGIN

Expand Down
42 changes: 42 additions & 0 deletions src/darwin/Framework/CHIP/MTRDeviceDataValidation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2024 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <Foundation/Foundation.h>

#import "MTRDefines_Internal.h"

NS_ASSUME_NONNULL_BEGIN

// Returns whether a data-value dictionary is well-formed (in the sense that all
// the types of the objects inside are as expected, so it's actually a valid
// representation of some TLV). Implemented in MTRBaseDevice.mm because that's
// where the pieces needed to implement it are, but declared here so our tests
// can see it.
MTR_EXTERN MTR_TESTABLE BOOL MTRDataValueDictionaryIsWellFormed(MTRDeviceDataValueDictionary value);

// Returns whether the provided attribute report actually has the right sorts of
// objects in the right places.
MTR_EXTERN MTR_TESTABLE BOOL MTRAttributeReportIsWellFormed(NSArray<MTRDeviceResponseValueDictionary> * report);

// Returns whether the provided event report actually has the right sorts of
// objects in the right places.
MTR_EXTERN MTR_TESTABLE BOOL MTREventReportIsWellFormed(NSArray<MTRDeviceResponseValueDictionary> * report);

// Returns whether the provided invoke response actually has the right sorts of
// objects in the right places.
MTR_EXTERN MTR_TESTABLE BOOL MTRInvokeResponseIsWellFormed(NSArray<MTRDeviceResponseValueDictionary> * response);

NS_ASSUME_NONNULL_END
216 changes: 216 additions & 0 deletions src/darwin/Framework/CHIP/MTRDeviceDataValidation.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Copyright (c) 2024 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import "MTRDeviceDataValidation.h"

#import "MTRBaseDevice.h"

#import "MTRLogging_Internal.h"

// MTRDataValueDictionaryIsWellFormed lives in MTRBaseDevice.mm, because it uses
// static functions defined in that file.

#pragma mark - Helpers used by multiple validators

#define MTR_CHECK_CLASS(className) \
^(className * arg) { return MTR_SAFE_CAST(arg, className) != nil; }

// input is not known to be an NSDictionary yet on entry.
//
// expectedShape maps keys to value validator blocks.
static BOOL MTRDictionaryHasExpectedShape(NSDictionary * input, NSDictionary * expectedShape)
{
if (!MTR_SAFE_CAST(input, NSDictionary)) {
return NO;
}

for (id key in expectedShape) {
id value = input[key];
if (!value) {
return NO;
}
auto validator = static_cast<BOOL (^)(id)>(expectedShape[key]);
if (!validator(value)) {
return NO;
}
}

return YES;
}

#pragma mark - Attribute report validation

static const auto sAttributeDataShape = @{
MTRAttributePathKey : MTR_CHECK_CLASS(MTRAttributePath),
MTRDataKey : (^(MTRDeviceDataValueDictionary arg) {
return MTRDataValueDictionaryIsWellFormed(arg);
}),
};

static const auto sAttributeErrorShape = @{
MTRAttributePathKey : MTR_CHECK_CLASS(MTRAttributePath),
MTRErrorKey : MTR_CHECK_CLASS(NSError),
};

BOOL MTRAttributeReportIsWellFormed(NSArray<MTRDeviceResponseValueDictionary> * report)
{
if (!MTR_SAFE_CAST(report, NSArray)) {
MTR_LOG_ERROR("Attribute report is not an array: %@", report);
return NO;
}

for (MTRDeviceResponseValueDictionary item in report) {
// item can be a value report or an error report.
if (!MTRDictionaryHasExpectedShape(item, sAttributeDataShape) && !MTRDictionaryHasExpectedShape(item, sAttributeErrorShape)) {
MTR_LOG_ERROR("Attribute report contains a weird entry: %@", item);
return NO;
}

// Now we know item is in fact a dictionary, and it has at least one of MTRDataKey and MTRErrorKey. Make sure it's
// not claiming both, which could confuse code that examines it.
if (item[MTRDataKey] != nil && item[MTRErrorKey] != nil) {
MTR_LOG_ERROR("Attribute report contains an entry that claims to be both data and error: %@", item);
return NO;
}
}

return YES;
}

#pragma mark - Event report validation

// MTREventIsHistoricalKey is claimed to be present no matter what, as
// long as MTREventPathKey is present.
static const auto sEventDataShape = @{
MTREventPathKey : MTR_CHECK_CLASS(MTREventPath),
MTRDataKey : (^(MTRDeviceDataValueDictionary arg) {
return MTRDataValueDictionaryIsWellFormed(arg);
}),
MTREventIsHistoricalKey : MTR_CHECK_CLASS(NSNumber),
MTREventNumberKey : MTR_CHECK_CLASS(NSNumber),
MTREventPriorityKey : MTR_CHECK_CLASS(NSNumber),
MTREventTimeTypeKey : MTR_CHECK_CLASS(NSNumber),
};

static const auto sEventErrorShape = @{
MTREventPathKey : MTR_CHECK_CLASS(MTREventPath),
MTRErrorKey : MTR_CHECK_CLASS(NSError),
MTREventIsHistoricalKey : MTR_CHECK_CLASS(NSNumber),
};

BOOL MTREventReportIsWellFormed(NSArray<MTRDeviceResponseValueDictionary> * report)
{
if (!MTR_SAFE_CAST(report, NSArray)) {
MTR_LOG_ERROR("Event report is not an array: %@", report);
return NO;
}

for (MTRDeviceResponseValueDictionary item in report) {
// item can be a value report or an error report.
if (!MTRDictionaryHasExpectedShape(item, sEventDataShape) && !MTRDictionaryHasExpectedShape(item, sEventErrorShape)) {
MTR_LOG_ERROR("Event report contains a weird entry: %@", item);
return NO;
}

// Now we know item is in fact a dictionary, and it has at least one of MTRDataKey and MTRErrorKey. Make sure it's
// not claiming both, which could confuse code that examines it.
if (item[MTRDataKey] != nil && item[MTRErrorKey] != nil) {
MTR_LOG_ERROR("Event report contains an entry that claims to be both data and error: %@", item);
return NO;
}

if (item[MTRDataKey]) {
// Check well-formedness of our timestamps. Note that we have
// already validated the type of item[MTREventTimeTypeKey].
uint64_t eventTimeType = [item[MTREventTimeTypeKey] unsignedLongLongValue];
switch (eventTimeType) {
case MTREventTimeTypeSystemUpTime: {
if (!MTR_SAFE_CAST(item[MTREventSystemUpTimeKey], NSNumber)) {
MTR_LOG_ERROR("Event report claims system uptime timing but does not have the time: %@", item);
return NO;
}
break;
}
case MTREventTimeTypeTimestampDate: {
if (!MTR_SAFE_CAST(item[MTREventTimestampDateKey], NSDate)) {
MTR_LOG_ERROR("Event report claims epoch timing but does not have the time: %@", item);
return NO;
}
break;
}
default:
MTR_LOG_ERROR("Uknown time type for event report: %@", item);
return NO;
}
}
}

return YES;
}

#pragma mark - Invoke response validation

BOOL MTRInvokeResponseIsWellFormed(NSArray<MTRDeviceResponseValueDictionary> * response)
{
if (!MTR_SAFE_CAST(response, NSArray)) {
MTR_LOG_ERROR("Invoke response is not an array: %@", response);
return NO;
}

// Input is an array with a single value that must have MTRCommandPathKey.
if (response.count != 1) {
MTR_LOG_ERROR("Invoke response is not an array with exactly one entry: %@", response);
return NO;
}

MTRDeviceResponseValueDictionary responseValue = response[0];

if (!MTR_SAFE_CAST(responseValue, NSDictionary) || !MTR_SAFE_CAST(responseValue[MTRCommandPathKey], MTRCommandPath)) {
MTR_LOG_ERROR("Invoke response is not an array with the right things in it: %@", response);
return NO;
}

MTRDeviceDataValueDictionary _Nullable data = responseValue[MTRDataKey];
NSError * _Nullable error = responseValue[MTRErrorKey];

if (data != nil && error != nil) {
MTR_LOG_ERROR("Invoke response claims to have both data and error: %@", responseValue);
return NO;
}

if (error != nil) {
return MTR_SAFE_CAST(error, NSError) != nil;
}

if (data == nil) {
// This is valid: indicates a success status response.
return YES;
}

if (!MTRDataValueDictionaryIsWellFormed(data)) {
MTR_LOG_ERROR("Invoke response claims to have data that is not a data-value: %@", data);
return NO;
}

// Now we know data is a dictionary (in fact a data-value). The only thing
// we promise about it is that it has type MTRStructureValueType.
if (data[MTRTypeKey] != MTRStructureValueType) {
MTR_LOG_ERROR("Invoke response data is not of structure type: %@", data);
return NO;
}

return YES;
}
26 changes: 0 additions & 26 deletions src/darwin/Framework/CHIP/MTRDeviceDataValueDictionary.h

This file was deleted.

16 changes: 13 additions & 3 deletions src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
#import "MTRDeviceConnectivityMonitor.h"
#import "MTRDeviceControllerOverXPC.h"
#import "MTRDeviceController_Internal.h"
#import "MTRDeviceDataValueDictionary.h"
#import "MTRDeviceDataValidation.h"
#import "MTRDevice_Concrete.h"
#import "MTRDevice_Internal.h"
#import "MTRError_Internal.h"
Expand Down Expand Up @@ -1899,8 +1899,13 @@ - (void)_handleAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attrib
}

// BEGIN DRAGON: This is used by the XPC Server to inject reports into local cache and broadcast them
- (void)_injectAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attributeReport fromSubscription:(BOOL)isFromSubscription
- (void)_injectAttributeReport:(NSArray<MTRDeviceResponseValueDictionary> *)attributeReport fromSubscription:(BOOL)isFromSubscription
{
if (!MTRAttributeReportIsWellFormed(attributeReport)) {
MTR_LOG_ERROR("%@ injected attribute report is not well-formed: %@", self, attributeReport);
return;
}

[_deviceController asyncDispatchToMatterQueue:^{
MTR_LOG("%@ injected attribute report (%p) %@", self, attributeReport, attributeReport);
[self _handleReportBegin];
Expand All @@ -1911,8 +1916,13 @@ - (void)_injectAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attrib
} errorHandler:nil];
}

- (void)_injectEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport
- (void)_injectEventReport:(NSArray<MTRDeviceResponseValueDictionary> *)eventReport
{
if (!MTREventReportIsWellFormed(eventReport)) {
MTR_LOG_ERROR("%@ injected event report is not well-formed: %@", self, eventReport);
return;
}

// [_deviceController asyncDispatchToMatterQueue:^{ // TODO: This wasn't used previously, not sure why, so keeping it here for thought, but preserving existing behavior
dispatch_async(self.queue, ^{
[self _handleEventReport:eventReport];
Expand Down
Loading

0 comments on commit b7aa537

Please sign in to comment.