Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/dom/media/gmp/GMPDiskStorage.cpp
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* This Source Code Form is subject to the terms of the Mozilla Public
3
 * License, v. 2.0. If a copy of the MPL was not distributed with this
4
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6
#include "plhash.h"
7
#include "nsDirectoryServiceUtils.h"
8
#include "nsDirectoryServiceDefs.h"
9
#include "nsAppDirectoryServiceDefs.h"
10
#include "GMPParent.h"
11
#include "gmp-storage.h"
12
#include "mozilla/Unused.h"
13
#include "mozilla/EndianUtils.h"
14
#include "nsClassHashtable.h"
15
#include "prio.h"
16
#include "mozIGeckoMediaPluginService.h"
17
#include "nsContentCID.h"
18
#include "nsServiceManagerUtils.h"
19
#include "nsISimpleEnumerator.h"
20
21
namespace mozilla {
22
23
#ifdef LOG
24
#undef LOG
25
#endif
26
27
extern LogModule* GetGMPLog();
28
29
#define LOGD(msg) MOZ_LOG(GetGMPLog(), mozilla::LogLevel::Debug, msg)
30
#define LOG(level, msg) MOZ_LOG(GetGMPLog(), (level), msg)
31
32
namespace gmp {
33
34
// We store the records for a given GMP as files in the profile dir.
35
// $profileDir/gmp/$platform/$gmpName/storage/$nodeId/
36
static nsresult
37
GetGMPStorageDir(nsIFile** aTempDir,
38
                 const nsString& aGMPName,
39
                 const nsCString& aNodeId)
40
0
{
41
0
  if (NS_WARN_IF(!aTempDir)) {
42
0
    return NS_ERROR_INVALID_ARG;
43
0
  }
44
0
45
0
  nsCOMPtr<mozIGeckoMediaPluginChromeService> mps =
46
0
    do_GetService("@mozilla.org/gecko-media-plugin-service;1");
47
0
  if (NS_WARN_IF(!mps)) {
48
0
    return NS_ERROR_FAILURE;
49
0
  }
50
0
51
0
  nsCOMPtr<nsIFile> tmpFile;
52
0
  nsresult rv = mps->GetStorageDir(getter_AddRefs(tmpFile));
53
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
54
0
    return rv;
55
0
  }
56
0
57
0
  rv = tmpFile->Append(aGMPName);
58
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
59
0
    return rv;
60
0
  }
61
0
62
0
  rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
63
0
  if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
64
0
    return rv;
65
0
  }
66
0
67
0
  rv = tmpFile->AppendNative(NS_LITERAL_CSTRING("storage"));
68
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
69
0
    return rv;
70
0
  }
71
0
72
0
  rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
73
0
  if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
74
0
    return rv;
75
0
  }
76
0
77
0
  rv = tmpFile->AppendNative(aNodeId);
78
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
79
0
    return rv;
80
0
  }
81
0
82
0
  rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
83
0
  if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
84
0
    return rv;
85
0
  }
86
0
87
0
  tmpFile.forget(aTempDir);
88
0
89
0
  return NS_OK;
90
0
}
91
92
// Disk-backed GMP storage. Records are stored in files on disk in
93
// the profile directory. The record name is a hash of the filename,
94
// and we resolve hash collisions by just adding 1 to the hash code.
95
// The format of records on disk is:
96
//   4 byte, uint32_t $recordNameLength, in little-endian byte order,
97
//   record name (i.e. $recordNameLength bytes, no null terminator)
98
//   record bytes (entire remainder of file)
99
class GMPDiskStorage : public GMPStorage {
100
public:
101
  explicit GMPDiskStorage(const nsCString& aNodeId,
102
                          const nsString& aGMPName)
103
    : mNodeId(aNodeId)
104
    , mGMPName(aGMPName)
105
0
  {
106
0
  }
107
108
0
  ~GMPDiskStorage() {
109
0
    // Close all open file handles.
110
0
    for (auto iter = mRecords.ConstIter(); !iter.Done(); iter.Next()) {
111
0
      Record* record = iter.UserData();
112
0
      if (record->mFileDesc) {
113
0
        PR_Close(record->mFileDesc);
114
0
        record->mFileDesc = nullptr;
115
0
      }
116
0
    }
117
0
  }
118
119
0
  nsresult Init() {
120
0
    // Build our index of records on disk.
121
0
    nsCOMPtr<nsIFile> storageDir;
122
0
    nsresult rv = GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId);
123
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
124
0
      return NS_ERROR_FAILURE;
125
0
    }
126
0
127
0
    DirectoryEnumerator iter(storageDir, DirectoryEnumerator::FilesAndDirs);
128
0
    for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) {
129
0
      PRFileDesc* fd = nullptr;
130
0
      if (NS_WARN_IF(
131
0
            NS_FAILED(dirEntry->OpenNSPRFileDesc(PR_RDONLY, 0, &fd)))) {
132
0
        continue;
133
0
      }
134
0
      int32_t recordLength = 0;
135
0
      nsCString recordName;
136
0
      nsresult err = ReadRecordMetadata(fd, recordLength, recordName);
137
0
      PR_Close(fd);
138
0
      if (NS_WARN_IF(NS_FAILED(err))) {
139
0
        // File is not a valid storage file. Don't index it. Delete the file,
140
0
        // to make our indexing faster in future.
141
0
        dirEntry->Remove(false);
142
0
        continue;
143
0
      }
144
0
145
0
      nsAutoString filename;
146
0
      rv = dirEntry->GetLeafName(filename);
147
0
      if (NS_WARN_IF(NS_FAILED(rv))) {
148
0
        continue;
149
0
      }
150
0
151
0
      mRecords.Put(recordName, new Record(filename, recordName));
152
0
    }
153
0
154
0
    return NS_OK;
155
0
  }
156
157
  GMPErr Open(const nsCString& aRecordName) override
158
0
  {
159
0
    MOZ_ASSERT(!IsOpen(aRecordName));
160
0
    nsresult rv;
161
0
    Record* record = nullptr;
162
0
    if (!mRecords.Get(aRecordName, &record)) {
163
0
      // New file.
164
0
      nsAutoString filename;
165
0
      rv = GetUnusedFilename(aRecordName, filename);
166
0
      if (NS_WARN_IF(NS_FAILED(rv))) {
167
0
        return GMPGenericErr;
168
0
      }
169
0
      record = new Record(filename, aRecordName);
170
0
      mRecords.Put(aRecordName, record);
171
0
    }
172
0
173
0
    MOZ_ASSERT(record);
174
0
    if (record->mFileDesc) {
175
0
      NS_WARNING("Tried to open already open record");
176
0
      return GMPRecordInUse;
177
0
    }
178
0
179
0
    rv = OpenStorageFile(record->mFilename, ReadWrite, &record->mFileDesc);
180
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
181
0
      return GMPGenericErr;
182
0
    }
183
0
184
0
    MOZ_ASSERT(IsOpen(aRecordName));
185
0
186
0
    return GMPNoErr;
187
0
  }
188
189
0
  bool IsOpen(const nsCString& aRecordName) const override {
190
0
    // We are open if we have a record indexed, and it has a valid
191
0
    // file descriptor.
192
0
    const Record* record = mRecords.Get(aRecordName);
193
0
    return record && !!record->mFileDesc;
194
0
  }
195
196
  GMPErr Read(const nsCString& aRecordName,
197
              nsTArray<uint8_t>& aOutBytes) override
198
0
  {
199
0
    if (!IsOpen(aRecordName)) {
200
0
      return GMPClosedErr;
201
0
    }
202
0
203
0
    Record* record = nullptr;
204
0
    mRecords.Get(aRecordName, &record);
205
0
    MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this.
206
0
207
0
    // Our error strategy is to report records with invalid contents as
208
0
    // containing 0 bytes. Zero length records are considered "deleted" by
209
0
    // the GMPStorage API.
210
0
    aOutBytes.SetLength(0);
211
0
212
0
    int32_t recordLength = 0;
213
0
    nsCString recordName;
214
0
    nsresult err = ReadRecordMetadata(record->mFileDesc,
215
0
                                      recordLength,
216
0
                                      recordName);
217
0
    if (NS_WARN_IF(NS_FAILED(err) || recordLength == 0)) {
218
0
      // We failed to read the record metadata. Or the record is 0 length.
219
0
      // Treat damaged records as empty.
220
0
      // ReadRecordMetadata() could fail if the GMP opened a new record and
221
0
      // tried to read it before anything was written to it..
222
0
      return GMPNoErr;
223
0
    }
224
0
225
0
    if (!aRecordName.Equals(recordName)) {
226
0
      NS_WARNING("Record file contains some other record's contents!");
227
0
      return GMPRecordCorrupted;
228
0
    }
229
0
230
0
    // After calling ReadRecordMetadata, we should be ready to read the
231
0
    // record data.
232
0
    if (PR_Available(record->mFileDesc) != recordLength) {
233
0
      NS_WARNING("Record file length mismatch!");
234
0
      return GMPRecordCorrupted;
235
0
    }
236
0
237
0
    aOutBytes.SetLength(recordLength);
238
0
    int32_t bytesRead = PR_Read(record->mFileDesc, aOutBytes.Elements(), recordLength);
239
0
    return (bytesRead == recordLength) ? GMPNoErr : GMPRecordCorrupted;
240
0
  }
241
242
  GMPErr Write(const nsCString& aRecordName,
243
               const nsTArray<uint8_t>& aBytes) override
244
0
  {
245
0
    if (!IsOpen(aRecordName)) {
246
0
      return GMPClosedErr;
247
0
    }
248
0
249
0
    Record* record = nullptr;
250
0
    mRecords.Get(aRecordName, &record);
251
0
    MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this.
252
0
253
0
    // Write operations overwrite the entire record. So close it now.
254
0
    PR_Close(record->mFileDesc);
255
0
    record->mFileDesc = nullptr;
256
0
257
0
    // Writing 0 bytes means removing (deleting) the file.
258
0
    if (aBytes.Length() == 0) {
259
0
      nsresult rv = RemoveStorageFile(record->mFilename);
260
0
      if (NS_WARN_IF(NS_FAILED(rv))) {
261
0
        // Could not delete file -> Continue with trying to erase the contents.
262
0
      } else {
263
0
        return GMPNoErr;
264
0
      }
265
0
    }
266
0
267
0
    // Write operations overwrite the entire record. So re-open the file
268
0
    // in truncate mode, to clear its contents.
269
0
    if (NS_WARN_IF(NS_FAILED(
270
0
          OpenStorageFile(record->mFilename, Truncate, &record->mFileDesc)))) {
271
0
      return GMPGenericErr;
272
0
    }
273
0
274
0
    // Store the length of the record name followed by the record name
275
0
    // at the start of the file.
276
0
    int32_t bytesWritten = 0;
277
0
    char buf[sizeof(uint32_t)] = {0};
278
0
    LittleEndian::writeUint32(buf, aRecordName.Length());
279
0
    bytesWritten = PR_Write(record->mFileDesc, buf, MOZ_ARRAY_LENGTH(buf));
280
0
    if (bytesWritten != MOZ_ARRAY_LENGTH(buf)) {
281
0
      NS_WARNING("Failed to write GMPStorage record name length.");
282
0
      return GMPRecordCorrupted;
283
0
    }
284
0
    bytesWritten = PR_Write(record->mFileDesc,
285
0
                            aRecordName.get(),
286
0
                            aRecordName.Length());
287
0
    if (bytesWritten != (int32_t)aRecordName.Length()) {
288
0
      NS_WARNING("Failed to write GMPStorage record name.");
289
0
      return GMPRecordCorrupted;
290
0
    }
291
0
292
0
    bytesWritten = PR_Write(record->mFileDesc, aBytes.Elements(), aBytes.Length());
293
0
    if (bytesWritten != (int32_t)aBytes.Length()) {
294
0
      NS_WARNING("Failed to write GMPStorage record data.");
295
0
      return GMPRecordCorrupted;
296
0
    }
297
0
298
0
    // Try to sync the file to disk, so that in the event of a crash,
299
0
    // the record is less likely to be corrupted.
300
0
    PR_Sync(record->mFileDesc);
301
0
302
0
    return GMPNoErr;
303
0
  }
304
305
  void Close(const nsCString& aRecordName) override
306
0
  {
307
0
    Record* record = nullptr;
308
0
    mRecords.Get(aRecordName, &record);
309
0
    if (record && !!record->mFileDesc) {
310
0
      PR_Close(record->mFileDesc);
311
0
      record->mFileDesc = nullptr;
312
0
    }
313
0
    MOZ_ASSERT(!IsOpen(aRecordName));
314
0
  }
315
316
private:
317
318
  // We store records in a file which is a hash of the record name.
319
  // If there is a hash collision, we just keep adding 1 to the hash
320
  // code, until we find a free slot.
321
  nsresult GetUnusedFilename(const nsACString& aRecordName,
322
                             nsString& aOutFilename)
323
0
  {
324
0
    nsCOMPtr<nsIFile> storageDir;
325
0
    nsresult rv = GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId);
326
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
327
0
      return rv;
328
0
    }
329
0
330
0
    uint64_t recordNameHash = HashString(PromiseFlatCString(aRecordName).get());
331
0
    for (int i = 0; i < 1000000; i++) {
332
0
      nsCOMPtr<nsIFile> f;
333
0
      rv = storageDir->Clone(getter_AddRefs(f));
334
0
      if (NS_WARN_IF(NS_FAILED(rv))) {
335
0
        return rv;
336
0
      }
337
0
      nsAutoString hashStr;
338
0
      hashStr.AppendInt(recordNameHash);
339
0
      rv = f->Append(hashStr);
340
0
      if (NS_WARN_IF(NS_FAILED(rv))) {
341
0
        return rv;
342
0
      }
343
0
      bool exists = false;
344
0
      f->Exists(&exists);
345
0
      if (!exists) {
346
0
        // Filename not in use, we can write into this file.
347
0
        aOutFilename = hashStr;
348
0
        return NS_OK;
349
0
      } else {
350
0
        // Hash collision; just increment the hash name and try that again.
351
0
        ++recordNameHash;
352
0
        continue;
353
0
      }
354
0
    }
355
0
    // Somehow, we've managed to completely fail to find a vacant file name.
356
0
    // Give up.
357
0
    NS_WARNING("GetUnusedFilename had extreme hash collision!");
358
0
    return NS_ERROR_FAILURE;
359
0
  }
360
361
  enum OpenFileMode  { ReadWrite, Truncate };
362
363
  nsresult OpenStorageFile(const nsAString& aFileLeafName,
364
                           const OpenFileMode aMode,
365
                           PRFileDesc** aOutFD)
366
0
  {
367
0
    MOZ_ASSERT(aOutFD);
368
0
369
0
    nsCOMPtr<nsIFile> f;
370
0
    nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId);
371
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
372
0
      return rv;
373
0
    }
374
0
    f->Append(aFileLeafName);
375
0
376
0
    auto mode = PR_RDWR | PR_CREATE_FILE;
377
0
    if (aMode == Truncate) {
378
0
      mode |= PR_TRUNCATE;
379
0
    }
380
0
381
0
    return f->OpenNSPRFileDesc(mode, PR_IRWXU, aOutFD);
382
0
  }
383
384
  nsresult ReadRecordMetadata(PRFileDesc* aFd,
385
                              int32_t& aOutRecordLength,
386
                              nsACString& aOutRecordName)
387
0
  {
388
0
    int32_t offset = PR_Seek(aFd, 0, PR_SEEK_END);
389
0
    PR_Seek(aFd, 0, PR_SEEK_SET);
390
0
391
0
    if (offset < 0 || offset > GMP_MAX_RECORD_SIZE) {
392
0
      // Refuse to read big records, or records where we can't get a length.
393
0
      return NS_ERROR_FAILURE;
394
0
    }
395
0
    const uint32_t fileLength = static_cast<uint32_t>(offset);
396
0
397
0
    // At the start of the file the length of the record name is stored in a
398
0
    // uint32_t (little endian byte order) followed by the record name at the
399
0
    // start of the file. The record name is not null terminated. The remainder
400
0
    // of the file is the record's data.
401
0
402
0
    if (fileLength < sizeof(uint32_t)) {
403
0
      // Record file doesn't have enough contents to store the record name
404
0
      // length. Fail.
405
0
      return NS_ERROR_FAILURE;
406
0
    }
407
0
408
0
    // Read length, and convert to host byte order.
409
0
    uint32_t recordNameLength = 0;
410
0
    char buf[sizeof(recordNameLength)] = { 0 };
411
0
    int32_t bytesRead = PR_Read(aFd, &buf, sizeof(recordNameLength));
412
0
    recordNameLength = LittleEndian::readUint32(buf);
413
0
    if (sizeof(recordNameLength) != bytesRead ||
414
0
        recordNameLength == 0 ||
415
0
        recordNameLength + sizeof(recordNameLength) > fileLength ||
416
0
        recordNameLength > GMP_MAX_RECORD_NAME_SIZE) {
417
0
      // Record file has invalid contents. Fail.
418
0
      return NS_ERROR_FAILURE;
419
0
    }
420
0
421
0
    nsCString recordName;
422
0
    recordName.SetLength(recordNameLength);
423
0
    bytesRead = PR_Read(aFd, recordName.BeginWriting(), recordNameLength);
424
0
    if ((uint32_t)bytesRead != recordNameLength) {
425
0
      // Read failed.
426
0
      return NS_ERROR_FAILURE;
427
0
    }
428
0
429
0
    MOZ_ASSERT(fileLength >= sizeof(recordNameLength) + recordNameLength);
430
0
    int32_t recordLength = fileLength - (sizeof(recordNameLength) + recordNameLength);
431
0
432
0
    aOutRecordLength = recordLength;
433
0
    aOutRecordName = recordName;
434
0
435
0
    // Read cursor should be positioned after the record name, before the record contents.
436
0
    if (PR_Seek(aFd, 0, PR_SEEK_CUR) != (int32_t)(sizeof(recordNameLength) + recordNameLength)) {
437
0
      NS_WARNING("Read cursor mismatch after ReadRecordMetadata()");
438
0
      return NS_ERROR_FAILURE;
439
0
    }
440
0
441
0
    return NS_OK;
442
0
  }
443
444
  nsresult RemoveStorageFile(const nsString& aFilename)
445
0
  {
446
0
    nsCOMPtr<nsIFile> f;
447
0
    nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId);
448
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
449
0
      return rv;
450
0
    }
451
0
    rv = f->Append(aFilename);
452
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
453
0
      return rv;
454
0
    }
455
0
    return f->Remove(/* bool recursive= */ false);
456
0
  }
457
458
  struct Record {
459
    Record(const nsAString& aFilename,
460
           const nsACString& aRecordName)
461
      : mFilename(aFilename)
462
      , mRecordName(aRecordName)
463
      , mFileDesc(0)
464
0
    {}
465
0
    ~Record() {
466
0
      MOZ_ASSERT(!mFileDesc);
467
0
    }
468
    nsString mFilename;
469
    nsCString mRecordName;
470
    PRFileDesc* mFileDesc;
471
  };
472
473
  // Hash record name to record data.
474
  nsClassHashtable<nsCStringHashKey, Record> mRecords;
475
  const nsCString mNodeId;
476
  const nsString mGMPName;
477
};
478
479
already_AddRefed<GMPStorage> CreateGMPDiskStorage(const nsCString& aNodeId,
480
                                                  const nsString& aGMPName)
481
0
{
482
0
  RefPtr<GMPDiskStorage> storage(new GMPDiskStorage(aNodeId, aGMPName));
483
0
  if (NS_FAILED(storage->Init())) {
484
0
    NS_WARNING("Failed to initialize on disk GMP storage");
485
0
    return nullptr;
486
0
  }
487
0
  return storage.forget();
488
0
}
489
490
} // namespace gmp
491
} // namespace mozilla