From 4c86e9edcd81a24058e952112ff8b118db6da84b Mon Sep 17 00:00:00 2001 From: Chris Pearce Date: Wed, 3 Dec 2014 13:36:00 +0100 Subject: [PATCH] Bug 1104970 - Store GMPStorage record names at the start of each record. r=jesup --- dom/media/gmp/GMPStorageParent.cpp | 158 ++++++++++++++++++++----- dom/media/gmp/gmp-api/gmp-storage.h | 8 +- dom/media/gtest/TestGMPCrossOrigin.cpp | 47 ++++++++ 3 files changed, 180 insertions(+), 33 deletions(-) diff --git a/dom/media/gmp/GMPStorageParent.cpp b/dom/media/gmp/GMPStorageParent.cpp index 090eeec518f..494702e2aa6 100644 --- a/dom/media/gmp/GMPStorageParent.cpp +++ b/dom/media/gmp/GMPStorageParent.cpp @@ -18,7 +18,6 @@ #include "mozIGeckoMediaPluginService.h" #include "nsContentCID.h" #include "nsServiceManagerUtils.h" -#include "mozilla/Base64.h" #include "nsISimpleEnumerator.h" namespace mozilla { @@ -106,17 +105,9 @@ OpenStorageFile(const nsCString& aRecordName, return rv; } - nsAutoCString recordNameBase64; - rv = Base64Encode(aRecordName, recordNameBase64); - if (NS_WARN_IF(NS_FAILED(rv))) { - return rv; - } - - // Base64 can encode to a '/' character, which will mess with file paths, - // so we need to replace that here with something that won't mess with paths. - recordNameBase64.ReplaceChar('/', '-'); - - f->AppendNative(recordNameBase64); + nsAutoString recordNameHash; + recordNameHash.AppendInt(HashString(aRecordName.get())); + f->Append(recordNameHash); auto mode = PR_RDWR | PR_CREATE_FILE; if (aMode == Truncate) { @@ -162,24 +153,100 @@ public: return mFiles.Contains(aRecordName); } + GMPErr ReadRecordMetadata(PRFileDesc* aFd, + int32_t& aOutFileLength, + int32_t& aOutRecordLength, + nsACString& aOutRecordName) + { + int32_t fileLength = PR_Seek(aFd, 0, PR_SEEK_END); + PR_Seek(aFd, 0, PR_SEEK_SET); + + if (fileLength > GMP_MAX_RECORD_SIZE) { + // Refuse to read big records. + return GMPQuotaExceededErr; + } + aOutFileLength = fileLength; + aOutRecordLength = 0; + + // At the start of the file the length of the record name is stored in a + // size_t (host byte order) followed by the record name at the start of + // the file. The record name is not null terminated. The remainder of the + // file is the record's data. + + size_t recordNameLength = 0; + if (fileLength == 0 || sizeof(recordNameLength) >= (size_t)fileLength) { + // Record file is empty, or doesn't even have enough contents to + // store the record name length and/or record name. Report record + // as empty. + return GMPNoErr; + } + + int32_t bytesRead = PR_Read(aFd, &recordNameLength, sizeof(recordNameLength)); + if (sizeof(recordNameLength) != bytesRead || + recordNameLength > fileLength - sizeof(recordNameLength)) { + // Record file has invalid contents. Report record as empty. + return GMPNoErr; + } + + nsCString recordName; + recordName.SetLength(recordNameLength); + bytesRead = PR_Read(aFd, recordName.BeginWriting(), recordNameLength); + if (bytesRead != (int32_t)recordNameLength) { + // Record file has invalid contents. Report record as empty. + return GMPGenericErr; + } + + MOZ_ASSERT(fileLength > 0 && (size_t)fileLength >= sizeof(recordNameLength) + recordNameLength); + int32_t recordLength = fileLength - (sizeof(recordNameLength) + recordNameLength); + + aOutRecordLength = recordLength; + aOutRecordName = recordName; + + return GMPNoErr; + } + virtual GMPErr Read(const nsCString& aRecordName, nsTArray& aOutBytes) MOZ_OVERRIDE { + // Our error strategy is to report records with invalid contents as + // containing 0 bytes. Zero length records are considered "deleted" by + // the GMPStorage API. + aOutBytes.SetLength(0); + PRFileDesc* fd = mFiles.Get(aRecordName); if (!fd) { return GMPGenericErr; } - int32_t len = PR_Seek(fd, 0, PR_SEEK_END); - PR_Seek(fd, 0, PR_SEEK_SET); - - if (len > GMP_MAX_RECORD_SIZE) { - // Refuse to read big records. - return GMPQuotaExceededErr; + int32_t fileLength = 0; + int32_t recordLength = 0; + nsCString recordName; + GMPErr err = ReadRecordMetadata(fd, + fileLength, + recordLength, + recordName); + if (NS_WARN_IF(GMP_FAILED(err))) { + return err; } - aOutBytes.SetLength(len); - auto bytesRead = PR_Read(fd, aOutBytes.Elements(), len); - return (bytesRead == len) ? GMPNoErr : GMPGenericErr; + + if (recordLength == 0) { + // Record is empoty but not invalid, or it's invalid and we're going to + // just act like it's empty and let the client overwrite it. + return GMPNoErr; + } + + if (!aRecordName.Equals(recordName)) { + NS_WARNING("Hash collision in GMPStorage"); + return GMPGenericErr; + } + + // After calling ReadRecordMetadata, we should be ready to read the + // record data. + MOZ_ASSERT(PR_Available(fd) == recordLength); + + aOutBytes.SetLength(recordLength); + int32_t bytesRead = PR_Read(fd, aOutBytes.Elements(), recordLength); + return (bytesRead == recordLength) ? GMPNoErr : GMPGenericErr; } virtual GMPErr Write(const nsCString& aRecordName, @@ -199,7 +266,22 @@ public: } mFiles.Put(aRecordName, fd); - int32_t bytesWritten = PR_Write(fd, aBytes.Elements(), aBytes.Length()); + // Store the length of the record name followed by the record name + // at the start of the file. + int32_t bytesWritten = 0; + if (aBytes.Length() > 0) { + size_t recordNameLength = aRecordName.Length(); + bytesWritten = PR_Write(fd, &recordNameLength, sizeof(recordNameLength)); + if (NS_WARN_IF(bytesWritten != sizeof(recordNameLength))) { + return GMPGenericErr; + } + bytesWritten = PR_Write(fd, aRecordName.get(), recordNameLength); + if (NS_WARN_IF(bytesWritten != (int32_t)recordNameLength)) { + return GMPGenericErr; + } + } + + bytesWritten = PR_Write(fd, aBytes.Elements(), aBytes.Length()); return (bytesWritten == (int32_t)aBytes.Length()) ? GMPNoErr : GMPGenericErr; } @@ -235,13 +317,31 @@ public: continue; } - // The record's file name is the Base64 encode of the record name, - // with '/' characters replaced with '-' characters. Base64 decode - // to extract the file name. - leafName.ReplaceChar('-', '/'); - nsAutoCString recordName; - rv = Base64Decode(leafName, recordName); - if (NS_WARN_IF(NS_FAILED(rv))) { + PRFileDesc* fd = nullptr; + if (NS_FAILED(dirEntry->OpenNSPRFileDesc(PR_RDONLY, 0, &fd))) { + continue; + } + int32_t fileLength = 0; + int32_t recordLength = 0; + nsCString recordName; + GMPErr err = ReadRecordMetadata(fd, + fileLength, + recordLength, + recordName); + PR_Close(fd); + if (NS_WARN_IF(GMP_FAILED(err))) { + return err; + } + + if (recordName.IsEmpty() || recordLength == 0) { + continue; + } + + // Ensure the file name is the hash of the record name stored in the + // record file. Otherwise it's not a valid record. + nsAutoCString recordNameHash; + recordNameHash.AppendInt(HashString(recordName.get())); + if (!recordNameHash.Equals(leafName)) { continue; } diff --git a/dom/media/gmp/gmp-api/gmp-storage.h b/dom/media/gmp/gmp-api/gmp-storage.h index 56045ecc16f..c070c01245c 100644 --- a/dom/media/gmp/gmp-api/gmp-storage.h +++ b/dom/media/gmp/gmp-api/gmp-storage.h @@ -20,11 +20,11 @@ #include "gmp-errors.h" #include -// Maximum size of a record, in bytes. -#define GMP_MAX_RECORD_SIZE (1024 * 1024 * 1024) +// Maximum size of a record, in bytes; 10 megabytes. +#define GMP_MAX_RECORD_SIZE (10 * 1024 * 1024) -// Maximum length of a record name. -#define GMP_MAX_RECORD_NAME_SIZE 200 +// Maximum length of a record name in bytes. +#define GMP_MAX_RECORD_NAME_SIZE 2000 // Provides basic per-origin storage for CDMs. GMPRecord instances can be // retrieved by calling GMPPlatformAPI->openstorage. Multiple GMPRecords diff --git a/dom/media/gtest/TestGMPCrossOrigin.cpp b/dom/media/gtest/TestGMPCrossOrigin.cpp index 0ea6cdd5d7d..98266cf5cc6 100644 --- a/dom/media/gtest/TestGMPCrossOrigin.cpp +++ b/dom/media/gtest/TestGMPCrossOrigin.cpp @@ -641,6 +641,48 @@ class GMPStorageTest : public GMPDecryptorProxyCallback TestGetRecordNames(false); } + void TestLongRecordNames() { + NS_NAMED_LITERAL_CSTRING(longRecordName, + "A_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_" + "long_record_name"); + + NS_NAMED_LITERAL_CSTRING(data, "Just_some_arbitrary_data."); + + MOZ_ASSERT(longRecordName.Length() < GMP_MAX_RECORD_NAME_SIZE); + MOZ_ASSERT(longRecordName.Length() > 260); // Windows MAX_PATH + + CreateDecryptor(NS_LITERAL_STRING("fuz.com"), + NS_LITERAL_STRING("baz.com"), + false); + + nsCString response("stored "); + response.Append(longRecordName); + response.AppendLiteral(" "); + response.Append(data); + Expect(response, NS_NewRunnableMethod(this, &GMPStorageTest::SetFinished)); + + nsCString update("store "); + update.Append(longRecordName); + update.AppendLiteral(" "); + update.Append(data); + Update(update); + } + void Expect(const nsCString& aMessage, nsIRunnable* aContinuation) { mExpected.AppendElement(ExpectedMessage(aMessage, aContinuation)); } @@ -822,3 +864,8 @@ TEST(GeckoMediaPlugins, GMPStorageGetRecordNamesPersistentStorage) { nsRefPtr runner = new GMPStorageTest(); runner->DoTest(&GMPStorageTest::GetRecordNamesPersistentStorage); } + +TEST(GeckoMediaPlugins, GMPStorageLongRecordNames) { + nsRefPtr runner = new GMPStorageTest(); + runner->DoTest(&GMPStorageTest::TestLongRecordNames); +}