Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some machinery for structural validation in MTRDevice_XPC and MTRDevice_Concrete. #36224

Merged
merged 3 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
{
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, @ {
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
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]) {
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
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);
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
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")) {
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
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")) {
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
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.
bzbarsky-apple marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading