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.

Validate things that get injected (that we got from the XPC transport) as well
as various bits in MTRDevice_XPC.
  • Loading branch information
bzbarsky-apple committed Oct 23, 2024
1 parent 42421c4 commit 15156f6
Show file tree
Hide file tree
Showing 5 changed files with 732 additions and 1 deletion.
245 changes: 245 additions & 0 deletions src/darwin/Framework/CHIP/MTRDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -759,3 +759,248 @@ - (void)invokeCommandWithEndpointID:(NSNumber *)endpointID
}

@end

static BOOL MTRArrayIsStructurallyValid(NSArray * input, NSArray * structure)
{
if (structure.count != 1) {
// Unexpected structure. Just claim NO; later we might give meaning to
// such things.
MTR_LOG_ERROR("Don't know how to make sense of array structure with more than one element: %@", structure);
return NO;
}

id elementStructure = structure[0];
for (id inputElement in input) {
if (!MTRInputIsStructurallyValid(inputElement, elementStructure)) {
MTR_LOG_ERROR("Array element not structurally valid: %@, %@", inputElement, elementStructure);
return NO;
}
}
return YES;
}

static BOOL MTRDictionaryIsStructurallyValid(NSDictionary * input, NSDictionary * structure)
{
for (id key in structure) {
id inputValue = input[key];
if (!inputValue) {
MTR_LOG_ERROR("Input does not have key %@: %@, %@", key, input, structure);
return NO;
}
id valueStructure = structure[key];
if (!MTRInputIsStructurallyValid(inputValue, valueStructure)) {
MTR_LOG_ERROR("Dictionary value not structurally valid: %@, %@", inputValue, valueStructure);
return NO;
}
}

return YES;
}

static BOOL MTRAttributeReportIsStructurallyValid(id input)
{
// Input is an array of values, but they could have slightly different
// structures, so we can't do a single MTRInputIsStructurallyValid to check
// that. Check the items one at a time.
if (![input isKindOfClass:NSArray.class]) {
MTR_LOG_ERROR("Attribute report is not an array: %@", input);
return NO;
}

NSArray * inputArray = input;
for (id item in inputArray) {
// item can be a value report or an error report.
if (!MTRInputIsStructurallyValid(item, @ {
MTRAttributePathKey : MTRAttributePath.class,
MTRDataKey : MTRInputStructureDataValue,
})
&& !MTRInputIsStructurallyValid(item, @ {
MTRAttributePathKey : MTRAttributePath.class,
MTRErrorKey : NSError.class,
})) {
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.
NSDictionary * itemDictionary = item;
if (itemDictionary[MTRDataKey] != nil && itemDictionary[MTRErrorKey] != nil) {
MTR_LOG_ERROR("Attribute report contains an entry that claims to be both data and error: %@", item);
return NO;
}
}

return YES;
}

static BOOL MTREventReportIsStructurallyValid(id input)
{
// Input is an array of values, but they could have slightly different
// structures, so we can't do a single MTRInputIsStructurallyValid to check
// that. Check the items one at a time.
if (![input isKindOfClass:NSArray.class]) {
MTR_LOG_ERROR("Event report is not an array: %@", input);
return NO;
}

NSArray * inputArray = input;
for (id item in inputArray) {
// item can be a value report or an error report.
// MTREventIsHistoricalKey is claimed to be present no matter what, as
// long as MTREventPathKey is present.
if (!MTRInputIsStructurallyValid(item, @ {
MTREventPathKey : MTREventPath.class,
MTRDataKey : MTRInputStructureDataValue,
MTREventIsHistoricalKey : NSNumber.class,
})
&& !MTRInputIsStructurallyValid(item, @ {
MTREventPathKey : MTREventPath.class,
MTRErrorKey : NSError.class,
MTREventIsHistoricalKey : NSNumber.class,
})) {
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.
NSDictionary * itemDictionary = item;
if (itemDictionary[MTRDataKey] != nil && itemDictionary[MTRErrorKey] != nil) {
MTR_LOG_ERROR("Event report contains an entry that claims to be both data and error: %@", item);
return NO;
}

if (itemDictionary[MTRDataKey]) {
// In this case, the structure is supposed to contain some more
// values.
if (!MTRInputIsStructurallyValid(itemDictionary, @ {
MTREventNumberKey : NSNumber.class,
MTREventPriorityKey : NSNumber.class,
MTREventTimeTypeKey : NSNumber.class,
})) {
MTR_LOG_ERROR("Event report fields that depend on MTRDataKey failed validation: %@", itemDictionary);
return NO;
}

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

return YES;
}

static BOOL MTRInvokeResponseIsStructurallyValid(id input)
{
// Input is an array with a single value that must have MTRCommandPathKey.
if (!MTRInputIsStructurallyValid(input, @[ @ { MTRCommandPathKey : MTRCommandPath.class } ])) {
MTR_LOG_ERROR("Invoke response is not an array with the right things in it: %@", input);
return NO;
}

NSArray * inputArray = input;
if (inputArray.count != 1) {
MTR_LOG_ERROR("Invoke response is not an array with exactly one entry: %@", input);
return NO;
}

// Note that we have already checked the one entry is a dictionary that has
// MTRCommandPathKey.
NSDictionary * responseValue = inputArray[0];
id data = responseValue[MTRDataKey];
id 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 MTRInputIsStructurallyValid(error, NSError.class);
}

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

if (!MTRInputIsStructurallyValid(data, MTRInputStructureDataValue)) {
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.
NSDictionary * dataDictionary = data;
if (dataDictionary[MTRTypeKey] != MTRStructureValueType) {
MTR_LOG_ERROR("Invoke response data is not of structure type: %@", data);
return NO;
}

return YES;
}

BOOL MTRInputIsStructurallyValid(id input, id structure)
{
if ([structure isEqual:MTRInputStructureDataValue]) {
return MTRDataValueDictionaryIsWellFormed(input);
}

if ([structure isEqual:MTRInputStructureAttributeReport]) {
return MTRAttributeReportIsStructurallyValid(input);
}

if ([structure isEqual:MTRInputStructureEventReport]) {
return MTREventReportIsStructurallyValid(input);
}

if ([structure isEqual:MTRInputStructureInvokeResponse]) {
return MTRInvokeResponseIsStructurallyValid(input);
}

// Literal dictionaries and arrays actually have classes that are not
// exactly NSArray.class and NSDictionary.class, so checking isKindOfClass
// against [structure class] needs to be avoided.
if ([structure isKindOfClass:NSDictionary.class] &&
[input isKindOfClass:NSDictionary.class]) {
return MTRDictionaryIsStructurallyValid(input, structure);
}

if ([structure isKindOfClass:NSArray.class] &&
[input isKindOfClass:NSArray.class]) {
return MTRArrayIsStructurallyValid(input, structure);
}

if ([structure class] != structure) {
// Not a class object, not sure what to do with it.
MTR_LOG_ERROR("Unknown structure value: %@", structure);
return NO;
}

if (![input isKindOfClass:structure]) {
MTR_LOG_ERROR("Input not of the right class: %@, %@", input, structure);
return NO;
}

return YES;
}
8 changes: 8 additions & 0 deletions src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,10 @@ - (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
{
if (!MTRInputIsStructurallyValid(attributeReport, @"attribute-report")) {
return;
}

[_deviceController asyncDispatchToMatterQueue:^{
MTR_LOG("%@ injected attribute report (%p) %@", self, attributeReport, attributeReport);
[self _handleReportBegin];
Expand All @@ -1901,6 +1905,10 @@ - (void)_injectAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attrib

- (void)_injectEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport
{
if (!MTRInputIsStructurallyValid(eventReport, @"event-report")) {
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
31 changes: 31 additions & 0 deletions src/darwin/Framework/CHIP/MTRDevice_Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,31 @@ MTR_DIRECT_MEMBERS
// can see it.
MTR_EXTERN MTR_TESTABLE BOOL MTRDataValueDictionaryIsWellFormed(MTRDeviceDataValueDictionary value);

// Checks whether the input's structure corresponds to the expected structure,
// using the following rules:
//
// 1) If structure is MTRInputStructureDataValue, checks that input is a
// well-formed data-value.
// 2) If structure is the MTRInputStructureAttributeReport, checks that input is
// a well-formed array of response-values representing attribute reports.
// 3) If structure is MTRInputStructureEventReport, checks that input is a
// well-formed array of response-values representing attribute reports.
// 4) If the structure is MTRInputStructureInvokeResponse, checks that the input is
// a well-formed array of a single response-value representing an invoke response.
// 5) If structure is a dictionary, checks that its keys are all present in
// the input, and have values that match the structure of the provided values.
// 6) If structure is an array, that array is expected to contain a single
// element, which is the structure expected from the array elements. Using
// multiple elements in structure will return NO, to allow adding new
// functionality here in the future as needed.
// 7) If structure is a Class object, checks that the input's class matches that.
// 8) In all other cases returns NO for now, until we decide on what
// semantics those structures should have.
//
// For example a structure of @{ @"abc": NSNumber.class } will check that input is
// a dictionary, that it has "abc" as a key, and that the value is an NSNumber.
MTR_EXTERN MTR_TESTABLE BOOL MTRInputIsStructurallyValid(id input, id structure);

#pragma mark - Constants

static NSString * const kDefaultSubscriptionPoolSizeOverrideKey = @"subscriptionPoolSizeOverride";
Expand All @@ -200,4 +225,10 @@ static NSString * const kMTRDeviceInternalPropertyLastSubscriptionAttemptWait =
static NSString * const kMTRDeviceInternalPropertyMostRecentReportTime = @"MTRDeviceInternalPropertyMostRecentReportTime";
static NSString * const kMTRDeviceInternalPropertyLastSubscriptionFailureTime = @"MTRDeviceInternalPropertyLastSubscriptionFailureTime";

// Constants used by MTRInputIsStructurallyValid
static NSString * const MTRInputStructureDataValue = @"data-value";
static NSString * const MTRInputStructureAttributeReport = @"attribute-report";
static NSString * const MTRInputStructureEventReport = @"event-report";
static NSString * const MTRInputStructureInvokeResponse = @"invoke-response";

NS_ASSUME_NONNULL_END
Loading

0 comments on commit 15156f6

Please sign in to comment.