Bug 1104970 - Store GMPStorage record names at the start of each record. r=jesup

This commit is contained in:
Chris Pearce 2014-12-03 13:36:00 +01:00
parent 65ae37ec73
commit 77a45da243
3 changed files with 180 additions and 33 deletions

View File

@ -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<uint8_t>& 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;
}

View File

@ -20,11 +20,11 @@
#include "gmp-errors.h"
#include <stdint.h>
// 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

View File

@ -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<GMPStorageTest> runner = new GMPStorageTest();
runner->DoTest(&GMPStorageTest::GetRecordNamesPersistentStorage);
}
TEST(GeckoMediaPlugins, GMPStorageLongRecordNames) {
nsRefPtr<GMPStorageTest> runner = new GMPStorageTest();
runner->DoTest(&GMPStorageTest::TestLongRecordNames);
}