New usage entries are moved lower after creation.

[ Merge of http://go/wvgerrit/124004 ]

When the CDM creates a new usage entry for an offline or streaming
license, the new entry is immediately moved to the lowest available
entry index that has been marked as vacant (kStorageTypeUnknown).

When a license is released, its meta data that is managed by the CDM
is cleared; however, the usage entry's index is marked vacant, but it
is not released.  This creates wasted entry space in the usage table.
Unfortunately, defragging the table is computationally expensive and
may not be able to actually free up much space depending on when it
is performed.

For a typical user, this will likely not be an issue as the table
can get quite large compared to the number of licenses an app uses
and the table is partially cleaned on each boot.

GTS tests, however, have reached a point where they fill the usage
table before all tests are complete.  This is causing many unexpected
failures for devices.  Most of these tests release their license, but
the CDM never reaches a state where it can clean up the table.

By moving newly created entries to the lowest available index directly
after creating the entries, the table never needs to grow unless all
entries are in use.  Clean up is now almost never required.

Bug: 180639135
Bug: 180638990
Bug: 180638530
Test: MediaDrmTest#testWidevineApi28
Change-Id: I1a68d90d51384094298b27037778747ce7435374
This commit is contained in:
Alex Dale
2021-04-29 11:00:51 -07:00
parent 023b06eded
commit 884550333d
3 changed files with 504 additions and 141 deletions

View File

@@ -15,6 +15,8 @@
namespace wvcdm {
namespace {
using TableLock = std::unique_lock<std::mutex>;
const std::string kEmptyString;
const wvcdm::CdmKeySetId kDummyKeySetId = "DummyKsid";
@@ -252,75 +254,39 @@ CdmResponseType UsageTableHeader::AddEntry(
CryptoSession* crypto_session, bool persistent_license,
const CdmKeySetId& key_set_id, const std::string& usage_info_file_name,
const CdmKeyResponse& license_message, uint32_t* usage_entry_number) {
LOGD("oec_session_id = %u, type = %s, current_size = %zu",
crypto_session->oec_session_id(),
LOGD("key_set_id = %s, type = %s, current_size = %zu", IdToString(key_set_id),
persistent_license ? "OfflineLicense" : "Streaming",
usage_entry_info_.size());
metrics::CryptoMetrics* metrics = crypto_session->GetCryptoMetrics();
if (metrics == nullptr) metrics = &alternate_crypto_metrics_;
TableLock auto_lock(usage_table_header_lock_);
CdmResponseType status = crypto_session->CreateUsageEntry(usage_entry_number);
CdmResponseType status = CreateEntry(crypto_session, usage_entry_number);
if (status == INSUFFICIENT_CRYPTO_RESOURCES) {
LOGW("Usage table may be full, releasing oldest entry: size = %zu",
usage_entry_info_.size());
status = ReleaseOldestEntry(metrics);
if (status == NO_ERROR) {
status = crypto_session->CreateUsageEntry(usage_entry_number);
status = CreateEntry(crypto_session, usage_entry_number);
}
}
if (status != NO_ERROR) return status;
LOGV("Locking to add entry");
std::unique_lock<std::mutex> auto_lock(usage_table_header_lock_);
if (*usage_entry_number < usage_entry_info_.size()) {
LOGE(
"New entry number is smaller than table size: "
"entry_info_number = %u, table_size = %zu",
*usage_entry_number, usage_entry_info_.size());
return USAGE_INVALID_NEW_ENTRY;
}
status = RelocateNewEntry(crypto_session, usage_entry_number);
if (status != NO_ERROR) return status;
if (*usage_entry_number > usage_entry_info_.size()) {
LOGW(
"New entry number is larger than table size, resizing: "
"entry_info_number = %u, table_size = %zu",
*usage_entry_number, usage_entry_info_.size());
const size_t number_of_entries = usage_entry_info_.size();
usage_entry_info_.resize(*usage_entry_number + 1);
for (size_t i = number_of_entries; i < usage_entry_info_.size() - 1; ++i) {
usage_entry_info_[i].Clear();
}
} else /* *usage_entry_number == usage_entry_info_.size() */ {
usage_entry_info_.resize(*usage_entry_number + 1);
}
usage_entry_info_[*usage_entry_number].storage_type =
persistent_license ? kStorageLicense : kStorageUsageInfo;
usage_entry_info_[*usage_entry_number].key_set_id = key_set_id;
usage_entry_info_[*usage_entry_number].last_use_time = GetCurrentTime();
if (!persistent_license) {
usage_entry_info_[*usage_entry_number].usage_info_file_name =
usage_info_file_name;
usage_entry_info_[*usage_entry_number].offline_license_expiry_time = 0;
if (persistent_license) {
SetOfflineEntryInfo(*usage_entry_number, key_set_id, license_message);
} else {
// Need to determine the expire time for offline licenses.
video_widevine::License license;
if (license_message.size() > 0 &&
ParseLicenseFromLicenseMessage(license_message, &license)) {
const video_widevine::License::Policy& policy = license.policy();
usage_entry_info_[*usage_entry_number].offline_license_expiry_time =
license.license_start_time() + policy.rental_duration_seconds() +
policy.playback_duration_seconds();
} else {
// If the license duration cannot be determined for any reason, it
// is assumed to last at most 33 days.
usage_entry_info_[*usage_entry_number].offline_license_expiry_time =
usage_entry_info_[*usage_entry_number].last_use_time +
kDefaultExpireDuration;
}
SetUsageInfoEntryInfo(*usage_entry_number, key_set_id,
usage_info_file_name);
}
status = RefitTable(crypto_session);
if (status != NO_ERROR) {
usage_entry_info_[*usage_entry_number].Clear();
return status;
}
// Call to update the usage table header, but don't store the usage
@@ -334,8 +300,6 @@ CdmResponseType UsageTableHeader::AddEntry(
usage_entry_info_[*usage_entry_number].Clear();
return status;
}
LOGI("usage_entry_number = %u", *usage_entry_number);
StoreTable(device_files_.get());
return NO_ERROR;
}
@@ -393,7 +357,14 @@ CdmResponseType UsageTableHeader::InvalidateEntry(
uint32_t usage_entry_number, bool defrag_table, DeviceFiles* device_files,
metrics::CryptoMetrics* metrics) {
LOGD("usage_entry_number = %u", usage_entry_number);
std::unique_lock<std::mutex> auto_lock(usage_table_header_lock_);
TableLock auto_lock(usage_table_header_lock_);
return InvalidateEntryInternal(usage_entry_number, defrag_table, device_files,
metrics);
}
CdmResponseType UsageTableHeader::InvalidateEntryInternal(
uint32_t usage_entry_number, bool defrag_table, DeviceFiles* device_files,
metrics::CryptoMetrics* metrics) {
// OEMCrypto does not have any concept of "deleting" an entry.
// Instead, the CDM marks the entry's meta data as invalid (storage
// type unknown) and then performs a "defrag" of the OEMCrypto table.
@@ -436,14 +407,12 @@ CdmResponseType UsageTableHeader::InvalidateEntry(
size_t UsageTableHeader::UsageInfoCount() const {
LOGV("Locking to count usage info (streaming license) entries");
std::unique_lock<std::mutex> auto_lock(usage_table_header_lock_);
return std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(),
EntryIsUsageInfo);
}
size_t UsageTableHeader::OfflineEntryCount() const {
LOGV("Locking to count offline license entries");
std::unique_lock<std::mutex> auto_lock(usage_table_header_lock_);
return std::count_if(usage_entry_info_.cbegin(), usage_entry_info_.cend(),
EntryIsOfflineLicense);
}
@@ -547,6 +516,151 @@ bool UsageTableHeader::DetermineTableCapacity(CryptoSession* crypto_session) {
return true;
}
CdmResponseType UsageTableHeader::CreateEntry(
CryptoSession* const crypto_session, uint32_t* usage_entry_number) {
const CdmResponseType status =
crypto_session->CreateUsageEntry(usage_entry_number);
if (status != NO_ERROR) return status;
// If the new entry number is smaller than expected, then the usage
// table may be out of sync or OEMCrypto has been rolled back.
// Not safe to continue.
if (*usage_entry_number < usage_entry_info_.size()) {
LOGE(
"New entry number is smaller than table size: "
"entry_info_number = %u, table_size = %zu",
*usage_entry_number, usage_entry_info_.size());
return USAGE_INVALID_NEW_ENTRY;
}
LOGI("usage_entry_number = %u", *usage_entry_number);
const size_t previous_size = usage_entry_info_.size();
usage_entry_info_.resize(*usage_entry_number + 1);
if (*usage_entry_number > previous_size) {
LOGW(
"New entry number is larger than table size, resizing: "
"entry_info_number = %u, table_size = %zu",
*usage_entry_number, previous_size);
for (size_t i = previous_size; i < usage_entry_info_.size() - 1; ++i) {
usage_entry_info_[i].Clear();
}
}
usage_entry_info_[*usage_entry_number].Clear();
return NO_ERROR;
}
CdmResponseType UsageTableHeader::RelocateNewEntry(
CryptoSession* const crypto_session, uint32_t* usage_entry_number) {
static constexpr uint32_t kMinimumEntryNumber = 0;
const uint32_t initial_entry_number = *usage_entry_number;
if (initial_entry_number == kMinimumEntryNumber) {
// First entry in the table.
return NO_ERROR;
}
uint32_t unoccupied_entry_number = initial_entry_number;
for (uint32_t i = kMinimumEntryNumber; i < initial_entry_number; i++) {
if (IsEntryUnoccupied(i)) {
unoccupied_entry_number = i;
break;
}
}
if (unoccupied_entry_number == initial_entry_number) {
// No open position.
return NO_ERROR;
}
const CdmResponseType status =
crypto_session->MoveUsageEntry(unoccupied_entry_number);
if (status == MOVE_USAGE_ENTRY_DESTINATION_IN_USE) {
// Not unexpected, there is a window of time between releasing the
// entry and closing the OEMCrypto session.
LOGD("Released entry still in use: index = %u", unoccupied_entry_number);
return NO_ERROR;
}
if (status != NO_ERROR) return status;
LOGI("Entry moved: from_index = %u, to_index = %u", initial_entry_number,
unoccupied_entry_number);
*usage_entry_number = unoccupied_entry_number;
usage_entry_info_[unoccupied_entry_number] =
std::move(usage_entry_info_[initial_entry_number]);
usage_entry_info_[initial_entry_number].Clear();
return NO_ERROR;
}
bool UsageTableHeader::IsEntryUnoccupied(
const uint32_t usage_entry_number) const {
if (usage_entry_info_[usage_entry_number].storage_type !=
kStorageTypeUnknown) {
return false;
}
// TODO(sigquit): Check that entry is not in use by another session.
// NOTE: The |storage_type| check will protect the integrity of the
// entry. Attempting to use an entry index that is used by another
// session is recoverable and will not affect any opened sessions.
return true;
}
void UsageTableHeader::SetOfflineEntryInfo(
const uint32_t usage_entry_number, const std::string& key_set_id,
const CdmKeyResponse& license_message) {
CdmUsageEntryInfo& entry_info = usage_entry_info_[usage_entry_number];
entry_info.Clear();
entry_info.storage_type = kStorageLicense;
entry_info.key_set_id = key_set_id;
entry_info.last_use_time = GetCurrentTime();
// Need to determine the expire time for offline licenses.
video_widevine::License license;
if (!license_message.empty() &&
ParseLicenseFromLicenseMessage(license_message, &license)) {
const video_widevine::License::Policy& policy = license.policy();
entry_info.offline_license_expiry_time = license.license_start_time() +
policy.rental_duration_seconds() +
policy.playback_duration_seconds();
} else {
// If the license duration cannot be determined for any reason, it
// is assumed to last at most 33 days.
entry_info.offline_license_expiry_time =
entry_info.last_use_time + kDefaultExpireDuration;
}
}
void UsageTableHeader::SetUsageInfoEntryInfo(
const uint32_t usage_entry_number, const std::string& key_set_id,
const std::string& usage_info_file_name) {
CdmUsageEntryInfo& entry_info = usage_entry_info_[usage_entry_number];
entry_info.Clear();
entry_info.storage_type = kStorageUsageInfo;
entry_info.key_set_id = key_set_id;
entry_info.last_use_time = GetCurrentTime();
entry_info.usage_info_file_name = usage_info_file_name;
}
CdmResponseType UsageTableHeader::RefitTable(
CryptoSession* const crypto_session) {
// Remove all unoccupied entries at end of the table.
uint32_t entries_to_remove = 0;
for (uint32_t i = 0; i < usage_entry_info_.size(); i++) {
const uint32_t usage_entry_number = usage_entry_info_.size() - i - 1;
if (!IsEntryUnoccupied(usage_entry_number)) break;
++entries_to_remove;
}
if (entries_to_remove == 0) return NO_ERROR;
const uint32_t new_size = usage_entry_info_.size() - entries_to_remove;
const CdmResponseType status = crypto_session->ShrinkUsageTableHeader(
requested_security_level_, new_size, &usage_table_header_);
if (status == SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE) {
// This error likely indicates that another session has released
// its entry via a call to InvalidateEntry(), but has yet to close
// its OEMCrypto session.
// Safe to assume table state is not invalidated.
LOGW("Unexpected entry in use: range = [%u, %zu]", new_size,
usage_entry_info_.size() - 1);
return NO_ERROR;
}
if (status != NO_ERROR) return status;
LOGD("Table shrunk: old_size = %zu, new_size = %u", usage_entry_info_.size(),
new_size);
usage_entry_info_.resize(new_size);
return NO_ERROR;
}
CdmResponseType UsageTableHeader::MoveEntry(
uint32_t from_usage_entry_number, const CdmUsageEntry& from_usage_entry,
uint32_t to_usage_entry_number, DeviceFiles* device_files,
@@ -1035,8 +1149,8 @@ CdmResponseType UsageTableHeader::ReleaseOldestEntry(
const CdmUsageEntryStorageType storage_type = usage_entry_info.storage_type;
const CdmResponseType status =
InvalidateEntry(entry_number_to_delete, /* defrag_table = */ true,
device_files_.get(), metrics);
InvalidateEntryInternal(entry_number_to_delete, /* defrag_table = */ true,
device_files_.get(), metrics);
if (status != NO_ERROR) {
LOGE("Failed to invalidate oldest entry: status = %d",
@@ -1051,7 +1165,7 @@ CdmResponseType UsageTableHeader::ReleaseOldestEntry(
// Test only method.
void UsageTableHeader::InvalidateEntryForTest(uint32_t usage_entry_number) {
LOGV("usage_entry_number = %u", usage_entry_number);
LOGD("usage_entry_number = %u", usage_entry_number);
if (usage_entry_number >= usage_entry_info_.size()) {
LOGE(
"Requested usage entry number is larger than table size: "
@@ -1188,8 +1302,6 @@ bool UsageTableHeader::LruUpgradeAllUsageEntries() {
}
bool UsageTableHeader::GetRemovalCandidate(uint32_t* entry_to_remove) {
LOGV("Locking to determine removal candidates");
std::unique_lock<std::mutex> auto_lock(usage_table_header_lock_);
const size_t lru_unexpired_threshold =
HasUnlimitedTableCapacity()
? kLruUnexpiredThresholdFraction * size()