// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. (function() { var internal = mojo.internal; var validationError = { NONE: 'VALIDATION_ERROR_NONE', MISALIGNED_OBJECT: 'VALIDATION_ERROR_MISALIGNED_OBJECT', ILLEGAL_MEMORY_RANGE: 'VALIDATION_ERROR_ILLEGAL_MEMORY_RANGE', UNEXPECTED_STRUCT_HEADER: 'VALIDATION_ERROR_UNEXPECTED_STRUCT_HEADER', UNEXPECTED_ARRAY_HEADER: 'VALIDATION_ERROR_UNEXPECTED_ARRAY_HEADER', ILLEGAL_HANDLE: 'VALIDATION_ERROR_ILLEGAL_HANDLE', UNEXPECTED_INVALID_HANDLE: 'VALIDATION_ERROR_UNEXPECTED_INVALID_HANDLE', ILLEGAL_POINTER: 'VALIDATION_ERROR_ILLEGAL_POINTER', UNEXPECTED_NULL_POINTER: 'VALIDATION_ERROR_UNEXPECTED_NULL_POINTER', ILLEGAL_INTERFACE_ID: 'VALIDATION_ERROR_ILLEGAL_INTERFACE_ID', UNEXPECTED_INVALID_INTERFACE_ID: 'VALIDATION_ERROR_UNEXPECTED_INVALID_INTERFACE_ID', MESSAGE_HEADER_INVALID_FLAGS: 'VALIDATION_ERROR_MESSAGE_HEADER_INVALID_FLAGS', MESSAGE_HEADER_MISSING_REQUEST_ID: 'VALIDATION_ERROR_MESSAGE_HEADER_MISSING_REQUEST_ID', DIFFERENT_SIZED_ARRAYS_IN_MAP: 'VALIDATION_ERROR_DIFFERENT_SIZED_ARRAYS_IN_MAP', INVALID_UNION_SIZE: 'VALIDATION_ERROR_INVALID_UNION_SIZE', UNEXPECTED_NULL_UNION: 'VALIDATION_ERROR_UNEXPECTED_NULL_UNION', UNKNOWN_ENUM_VALUE: 'VALIDATION_ERROR_UNKNOWN_ENUM_VALUE', }; var NULL_MOJO_POINTER = "NULL_MOJO_POINTER"; var gValidationErrorObserver = null; function reportValidationError(error) { if (gValidationErrorObserver) { gValidationErrorObserver.lastError = error; } else { console.warn('Invalid message: ' + error); } } var ValidationErrorObserverForTesting = (function() { function Observer() { this.lastError = validationError.NONE; } Observer.prototype.reset = function() { this.lastError = validationError.NONE; }; return { getInstance: function() { if (!gValidationErrorObserver) { gValidationErrorObserver = new Observer(); } return gValidationErrorObserver; } }; })(); function isTestingMode() { return Boolean(gValidationErrorObserver); } function clearTestingMode() { gValidationErrorObserver = null; } function isEnumClass(cls) { return cls instanceof internal.Enum; } function isStringClass(cls) { return cls === internal.String || cls === internal.NullableString; } function isHandleClass(cls) { return cls === internal.Handle || cls === internal.NullableHandle; } function isInterfaceClass(cls) { return cls instanceof internal.Interface; } function isInterfaceRequestClass(cls) { return cls === internal.InterfaceRequest || cls === internal.NullableInterfaceRequest; } function isAssociatedInterfaceClass(cls) { return cls === internal.AssociatedInterfacePtrInfo || cls === internal.NullableAssociatedInterfacePtrInfo; } function isAssociatedInterfaceRequestClass(cls) { return cls === internal.AssociatedInterfaceRequest || cls === internal.NullableAssociatedInterfaceRequest; } function isNullable(type) { return type === internal.NullableString || type === internal.NullableHandle || type === internal.NullableAssociatedInterfacePtrInfo || type === internal.NullableAssociatedInterfaceRequest || type === internal.NullableInterface || type === internal.NullableInterfaceRequest || type instanceof internal.NullableArrayOf || type instanceof internal.NullablePointerTo; } function Validator(message) { this.message = message; this.offset = 0; this.handleIndex = 0; this.associatedEndpointHandleIndex = 0; this.payloadInterfaceIds = null; this.offsetLimit = this.message.buffer.byteLength; } Object.defineProperty(Validator.prototype, "handleIndexLimit", { get: function() { return this.message.handles.length; } }); Object.defineProperty(Validator.prototype, "associatedHandleIndexLimit", { get: function() { return this.payloadInterfaceIds ? this.payloadInterfaceIds.length : 0; } }); // True if we can safely allocate a block of bytes from start to // to start + numBytes. Validator.prototype.isValidRange = function(start, numBytes) { // Only positive JavaScript integers that are less than 2^53 // (Number.MAX_SAFE_INTEGER) can be represented exactly. if (start < this.offset || numBytes <= 0 || !Number.isSafeInteger(start) || !Number.isSafeInteger(numBytes)) return false; var newOffset = start + numBytes; if (!Number.isSafeInteger(newOffset) || newOffset > this.offsetLimit) return false; return true; }; Validator.prototype.claimRange = function(start, numBytes) { if (this.isValidRange(start, numBytes)) { this.offset = start + numBytes; return true; } return false; }; Validator.prototype.claimHandle = function(index) { if (index === internal.kEncodedInvalidHandleValue) return true; if (index < this.handleIndex || index >= this.handleIndexLimit) return false; // This is safe because handle indices are uint32. this.handleIndex = index + 1; return true; }; Validator.prototype.claimAssociatedEndpointHandle = function(index) { if (index === internal.kEncodedInvalidHandleValue) { return true; } if (index < this.associatedEndpointHandleIndex || index >= this.associatedHandleIndexLimit) { return false; } // This is safe because handle indices are uint32. this.associatedEndpointHandleIndex = index + 1; return true; }; Validator.prototype.validateEnum = function(offset, enumClass) { // Note: Assumes that enums are always 32 bits! But this matches // mojom::generate::pack::PackedField::GetSizeForKind, so it should be okay. var value = this.message.buffer.getInt32(offset); return enumClass.validate(value); } Validator.prototype.validateHandle = function(offset, nullable) { var index = this.message.buffer.getUint32(offset); if (index === internal.kEncodedInvalidHandleValue) return nullable ? validationError.NONE : validationError.UNEXPECTED_INVALID_HANDLE; if (!this.claimHandle(index)) return validationError.ILLEGAL_HANDLE; return validationError.NONE; }; Validator.prototype.validateAssociatedEndpointHandle = function(offset, nullable) { var index = this.message.buffer.getUint32(offset); if (index === internal.kEncodedInvalidHandleValue) { return nullable ? validationError.NONE : validationError.UNEXPECTED_INVALID_INTERFACE_ID; } if (!this.claimAssociatedEndpointHandle(index)) { return validationError.ILLEGAL_INTERFACE_ID; } return validationError.NONE; }; Validator.prototype.validateInterface = function(offset, nullable) { return this.validateHandle(offset, nullable); }; Validator.prototype.validateInterfaceRequest = function(offset, nullable) { return this.validateHandle(offset, nullable); }; Validator.prototype.validateAssociatedInterface = function(offset, nullable) { return this.validateAssociatedEndpointHandle(offset, nullable); }; Validator.prototype.validateAssociatedInterfaceRequest = function( offset, nullable) { return this.validateAssociatedEndpointHandle(offset, nullable); }; Validator.prototype.validateStructHeader = function(offset, minNumBytes) { if (!internal.isAligned(offset)) return validationError.MISALIGNED_OBJECT; if (!this.isValidRange(offset, internal.kStructHeaderSize)) return validationError.ILLEGAL_MEMORY_RANGE; var numBytes = this.message.buffer.getUint32(offset); if (numBytes < minNumBytes) return validationError.UNEXPECTED_STRUCT_HEADER; if (!this.claimRange(offset, numBytes)) return validationError.ILLEGAL_MEMORY_RANGE; return validationError.NONE; }; Validator.prototype.validateStructVersion = function(offset, versionSizes) { var numBytes = this.message.buffer.getUint32(offset); var version = this.message.buffer.getUint32(offset + 4); if (version <= versionSizes[versionSizes.length - 1].version) { // Scan in reverse order to optimize for more recent versionSizes. for (var i = versionSizes.length - 1; i >= 0; --i) { if (version >= versionSizes[i].version) { if (numBytes == versionSizes[i].numBytes) break; return validationError.UNEXPECTED_STRUCT_HEADER; } } } else if (numBytes < versionSizes[versionSizes.length-1].numBytes) { return validationError.UNEXPECTED_STRUCT_HEADER; } return validationError.NONE; }; Validator.prototype.isFieldInStructVersion = function(offset, fieldVersion) { var structVersion = this.message.buffer.getUint32(offset + 4); return fieldVersion <= structVersion; }; Validator.prototype.validateMessageHeader = function() { var err = this.validateStructHeader(0, internal.kMessageV0HeaderSize); if (err != validationError.NONE) { return err; } var numBytes = this.message.getHeaderNumBytes(); var version = this.message.getHeaderVersion(); var validVersionAndNumBytes = (version == 0 && numBytes == internal.kMessageV0HeaderSize) || (version == 1 && numBytes == internal.kMessageV1HeaderSize) || (version == 2 && numBytes == internal.kMessageV2HeaderSize) || (version > 2 && numBytes >= internal.kMessageV2HeaderSize); if (!validVersionAndNumBytes) { return validationError.UNEXPECTED_STRUCT_HEADER; } var expectsResponse = this.message.expectsResponse(); var isResponse = this.message.isResponse(); if (version == 0 && (expectsResponse || isResponse)) { return validationError.MESSAGE_HEADER_MISSING_REQUEST_ID; } if (isResponse && expectsResponse) { return validationError.MESSAGE_HEADER_INVALID_FLAGS; } if (version < 2) { return validationError.NONE; } var err = this.validateArrayPointer( internal.kMessagePayloadInterfaceIdsPointerOffset, internal.Uint32.encodedSize, internal.Uint32, true, [0], 0); if (err != validationError.NONE) { return err; } this.payloadInterfaceIds = this.message.getPayloadInterfaceIds(); if (this.payloadInterfaceIds) { for (var interfaceId of this.payloadInterfaceIds) { if (!internal.isValidInterfaceId(interfaceId) || internal.isMasterInterfaceId(interfaceId)) { return validationError.ILLEGAL_INTERFACE_ID; } } } // Set offset to the start of the payload and offsetLimit to the start of // the payload interface Ids so that payload can be validated using the // same messageValidator. this.offset = this.message.getHeaderNumBytes(); this.offsetLimit = this.decodePointer( internal.kMessagePayloadInterfaceIdsPointerOffset); return validationError.NONE; }; Validator.prototype.validateMessageIsRequestWithoutResponse = function() { if (this.message.isResponse() || this.message.expectsResponse()) { return validationError.MESSAGE_HEADER_INVALID_FLAGS; } return validationError.NONE; }; Validator.prototype.validateMessageIsRequestExpectingResponse = function() { if (this.message.isResponse() || !this.message.expectsResponse()) { return validationError.MESSAGE_HEADER_INVALID_FLAGS; } return validationError.NONE; }; Validator.prototype.validateMessageIsResponse = function() { if (this.message.expectsResponse() || !this.message.isResponse()) { return validationError.MESSAGE_HEADER_INVALID_FLAGS; } return validationError.NONE; }; // Returns the message.buffer relative offset this pointer "points to", // NULL_MOJO_POINTER if the pointer represents a null, or JS null if the // pointer's value is not valid. Validator.prototype.decodePointer = function(offset) { var pointerValue = this.message.buffer.getUint64(offset); if (pointerValue === 0) return NULL_MOJO_POINTER; var bufferOffset = offset + pointerValue; return Number.isSafeInteger(bufferOffset) ? bufferOffset : null; }; Validator.prototype.decodeUnionSize = function(offset) { return this.message.buffer.getUint32(offset); }; Validator.prototype.decodeUnionTag = function(offset) { return this.message.buffer.getUint32(offset + 4); }; Validator.prototype.validateArrayPointer = function( offset, elementSize, elementType, nullable, expectedDimensionSizes, currentDimension) { var arrayOffset = this.decodePointer(offset); if (arrayOffset === null) return validationError.ILLEGAL_POINTER; if (arrayOffset === NULL_MOJO_POINTER) return nullable ? validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; return this.validateArray(arrayOffset, elementSize, elementType, expectedDimensionSizes, currentDimension); }; Validator.prototype.validateStructPointer = function( offset, structClass, nullable) { var structOffset = this.decodePointer(offset); if (structOffset === null) return validationError.ILLEGAL_POINTER; if (structOffset === NULL_MOJO_POINTER) return nullable ? validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; return structClass.validate(this, structOffset); }; Validator.prototype.validateUnion = function( offset, unionClass, nullable) { var size = this.message.buffer.getUint32(offset); if (size == 0) { return nullable ? validationError.NONE : validationError.UNEXPECTED_NULL_UNION; } return unionClass.validate(this, offset); }; Validator.prototype.validateNestedUnion = function( offset, unionClass, nullable) { var unionOffset = this.decodePointer(offset); if (unionOffset === null) return validationError.ILLEGAL_POINTER; if (unionOffset === NULL_MOJO_POINTER) return nullable ? validationError.NONE : validationError.UNEXPECTED_NULL_UNION; return this.validateUnion(unionOffset, unionClass, nullable); }; // This method assumes that the array at arrayPointerOffset has // been validated. Validator.prototype.arrayLength = function(arrayPointerOffset) { var arrayOffset = this.decodePointer(arrayPointerOffset); return this.message.buffer.getUint32(arrayOffset + 4); }; Validator.prototype.validateMapPointer = function( offset, mapIsNullable, keyClass, valueClass, valueIsNullable) { // Validate the implicit map struct: // struct {array keys; array values}; var structOffset = this.decodePointer(offset); if (structOffset === null) return validationError.ILLEGAL_POINTER; if (structOffset === NULL_MOJO_POINTER) return mapIsNullable ? validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; var mapEncodedSize = internal.kStructHeaderSize + internal.kMapStructPayloadSize; var err = this.validateStructHeader(structOffset, mapEncodedSize); if (err !== validationError.NONE) return err; // Validate the keys array. var keysArrayPointerOffset = structOffset + internal.kStructHeaderSize; err = this.validateArrayPointer( keysArrayPointerOffset, keyClass.encodedSize, keyClass, false, [0], 0); if (err !== validationError.NONE) return err; // Validate the values array. var valuesArrayPointerOffset = keysArrayPointerOffset + 8; var valuesArrayDimensions = [0]; // Validate the actual length below. if (valueClass instanceof internal.ArrayOf) valuesArrayDimensions = valuesArrayDimensions.concat(valueClass.dimensions()); var err = this.validateArrayPointer(valuesArrayPointerOffset, valueClass.encodedSize, valueClass, valueIsNullable, valuesArrayDimensions, 0); if (err !== validationError.NONE) return err; // Validate the lengths of the keys and values arrays. var keysArrayLength = this.arrayLength(keysArrayPointerOffset); var valuesArrayLength = this.arrayLength(valuesArrayPointerOffset); if (keysArrayLength != valuesArrayLength) return validationError.DIFFERENT_SIZED_ARRAYS_IN_MAP; return validationError.NONE; }; Validator.prototype.validateStringPointer = function(offset, nullable) { return this.validateArrayPointer( offset, internal.Uint8.encodedSize, internal.Uint8, nullable, [0], 0); }; // Similar to Array_Data::Validate() // mojo/public/cpp/bindings/lib/array_internal.h Validator.prototype.validateArray = function (offset, elementSize, elementType, expectedDimensionSizes, currentDimension) { if (!internal.isAligned(offset)) return validationError.MISALIGNED_OBJECT; if (!this.isValidRange(offset, internal.kArrayHeaderSize)) return validationError.ILLEGAL_MEMORY_RANGE; var numBytes = this.message.buffer.getUint32(offset); var numElements = this.message.buffer.getUint32(offset + 4); // Note: this computation is "safe" because elementSize <= 8 and // numElements is a uint32. var elementsTotalSize = (elementType === internal.PackedBool) ? Math.ceil(numElements / 8) : (elementSize * numElements); if (numBytes < internal.kArrayHeaderSize + elementsTotalSize) return validationError.UNEXPECTED_ARRAY_HEADER; if (expectedDimensionSizes[currentDimension] != 0 && numElements != expectedDimensionSizes[currentDimension]) { return validationError.UNEXPECTED_ARRAY_HEADER; } if (!this.claimRange(offset, numBytes)) return validationError.ILLEGAL_MEMORY_RANGE; // Validate the array's elements if they are pointers or handles. var elementsOffset = offset + internal.kArrayHeaderSize; var nullable = isNullable(elementType); if (isHandleClass(elementType)) return this.validateHandleElements(elementsOffset, numElements, nullable); if (isInterfaceClass(elementType)) return this.validateInterfaceElements( elementsOffset, numElements, nullable); if (isInterfaceRequestClass(elementType)) return this.validateInterfaceRequestElements( elementsOffset, numElements, nullable); if (isAssociatedInterfaceClass(elementType)) return this.validateAssociatedInterfaceElements( elementsOffset, numElements, nullable); if (isAssociatedInterfaceRequestClass(elementType)) return this.validateAssociatedInterfaceRequestElements( elementsOffset, numElements, nullable); if (isStringClass(elementType)) return this.validateArrayElements( elementsOffset, numElements, internal.Uint8, nullable, [0], 0); if (elementType instanceof internal.PointerTo) return this.validateStructElements( elementsOffset, numElements, elementType.cls, nullable); if (elementType instanceof internal.ArrayOf) return this.validateArrayElements( elementsOffset, numElements, elementType.cls, nullable, expectedDimensionSizes, currentDimension + 1); if (isEnumClass(elementType)) return this.validateEnumElements(elementsOffset, numElements, elementType.cls); return validationError.NONE; }; // Note: the |offset + i * elementSize| computation in the validateFooElements // methods below is "safe" because elementSize <= 8, offset and // numElements are uint32, and 0 <= i < numElements. Validator.prototype.validateHandleElements = function(offset, numElements, nullable) { var elementSize = internal.Handle.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateHandle(elementOffset, nullable); if (err != validationError.NONE) return err; } return validationError.NONE; }; Validator.prototype.validateInterfaceElements = function(offset, numElements, nullable) { var elementSize = internal.Interface.prototype.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateInterface(elementOffset, nullable); if (err != validationError.NONE) return err; } return validationError.NONE; }; Validator.prototype.validateInterfaceRequestElements = function(offset, numElements, nullable) { var elementSize = internal.InterfaceRequest.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateInterfaceRequest(elementOffset, nullable); if (err != validationError.NONE) return err; } return validationError.NONE; }; Validator.prototype.validateAssociatedInterfaceElements = function(offset, numElements, nullable) { var elementSize = internal.AssociatedInterfacePtrInfo.prototype.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateAssociatedInterface(elementOffset, nullable); if (err != validationError.NONE) { return err; } } return validationError.NONE; }; Validator.prototype.validateAssociatedInterfaceRequestElements = function(offset, numElements, nullable) { var elementSize = internal.AssociatedInterfaceRequest.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateAssociatedInterfaceRequest(elementOffset, nullable); if (err != validationError.NONE) { return err; } } return validationError.NONE; }; // The elementClass parameter is the element type of the element arrays. Validator.prototype.validateArrayElements = function(offset, numElements, elementClass, nullable, expectedDimensionSizes, currentDimension) { var elementSize = internal.PointerTo.prototype.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateArrayPointer( elementOffset, elementClass.encodedSize, elementClass, nullable, expectedDimensionSizes, currentDimension); if (err != validationError.NONE) return err; } return validationError.NONE; }; Validator.prototype.validateStructElements = function(offset, numElements, structClass, nullable) { var elementSize = internal.PointerTo.prototype.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateStructPointer(elementOffset, structClass, nullable); if (err != validationError.NONE) return err; } return validationError.NONE; }; Validator.prototype.validateEnumElements = function(offset, numElements, enumClass) { var elementSize = internal.Enum.prototype.encodedSize; for (var i = 0; i < numElements; i++) { var elementOffset = offset + i * elementSize; var err = this.validateEnum(elementOffset, enumClass); if (err != validationError.NONE) return err; } return validationError.NONE; }; internal.validationError = validationError; internal.Validator = Validator; internal.ValidationErrorObserverForTesting = ValidationErrorObserverForTesting; internal.reportValidationError = reportValidationError; internal.isTestingMode = isTestingMode; internal.clearTestingMode = clearTestingMode; })();