| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/variations/variations_layers.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <memory> |
| |
| #include "base/check_op.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "components/variations/entropy_provider.h" |
| |
| namespace variations { |
| |
| namespace { |
| |
| void LogInvalidLayerReason(InvalidLayerReason reason) { |
| base::UmaHistogramEnumeration("Variations.InvalidLayerReason", reason); |
| } |
| |
| // Iterates through the members of the given layer proto definition, and |
| // returns the member which contains that slot (if any). |
| const Layer::LayerMember* FindActiveMemberBySlot(uint32_t chosen_slot, |
| const Layer& layer_proto) { |
| for (const Layer::LayerMember& member : layer_proto.members()) { |
| if (!member.id()) |
| continue; |
| |
| for (const Layer::LayerMember::SlotRange& slot : member.slots()) { |
| if (slot.start() <= chosen_slot && chosen_slot <= slot.end()) |
| return &member; |
| } |
| } |
| return nullptr; |
| } |
| |
| // The result of SelectSlot. |
| struct SlotSelection { |
| // The slot selected. |
| ValueInRange slot; |
| // The remainder after dividing pseudorandom range to slots. |
| ValueInRange pseudorandom_remainder; |
| }; |
| |
| SlotSelection SelectSlot(ValueInRange pseudorandom, uint32_t num_slots) { |
| DCHECK_GT(pseudorandom.range, 0u); |
| DCHECK_GT(num_slots, 0u); |
| DCHECK_EQ(pseudorandom.range % num_slots, 0u); |
| // Since range and num_slots are both non-zero, and num_slots is a divisor of |
| // range, slot_size is also guaranteed to be non-zero. |
| uint32_t slot_size = pseudorandom.range / num_slots; |
| return { |
| .slot = |
| { |
| .value = pseudorandom.value / slot_size, |
| .range = num_slots, |
| }, |
| .pseudorandom_remainder = |
| { |
| .value = pseudorandom.value % slot_size, |
| .range = slot_size, |
| }, |
| }; |
| } |
| |
| ValueInRange CombineRanges(ValueInRange major, ValueInRange minor) { |
| return { |
| .value = major.value * minor.range + minor.value, |
| .range = major.range * minor.range, |
| }; |
| } |
| |
| ValueInRange SlotOfMember(const Layer::LayerMember& chosen_member, |
| uint32_t chosen_slot) { |
| uint32_t slots_in_member = 0; |
| uint32_t slots_in_member_less_than_chosen_slot = 0; |
| for (const Layer::LayerMember::SlotRange& range : chosen_member.slots()) { |
| const uint32_t range_size = range.end() - range.start() + 1; |
| slots_in_member += range_size; |
| if (chosen_slot > range.end()) { |
| slots_in_member_less_than_chosen_slot += range_size; |
| } else if (chosen_slot > range.start()) { |
| slots_in_member_less_than_chosen_slot += chosen_slot - range.start(); |
| } |
| } |
| return { |
| .value = slots_in_member_less_than_chosen_slot, |
| .range = slots_in_member, |
| }; |
| } |
| |
| // Computes a new entropy provider that can be used for uniform low-entropy |
| // randomization of studies in the layer member. |
| // |
| // The concept here is that the layer "divides" the pseudorandom range into |
| // different members, where "which member" is the "quotient", and now we are |
| // extracting the "remainder" of that division (as well as the range of the |
| // remainder, which will be the domain of the new provider). |
| // |
| // We define the remainder more specifically as the number of values in the |
| // pseudorandom function's range which give the same quotient (member) which are |
| // less than the given pseudorandom value. This makes the range of the |
| // remainder be the number of values in the range that map to the member. |
| // |
| // For example if |range| is [0,10) and we have a layer with 5 slots, and |
| // member M that contains slots 0 and 3, then there are 4 values in |range| |
| // that will activate that member [0,1,6,7], so the |remainder.range| will be 4. |
| // If |pseudorandom.value| is 7, then [0,1,6] are less than 7, so the |
| // |remainder.value| will be 3. |
| // |
| // The remainder is undefined for values not actually selected by the member, |
| // and this function should not be called with a chosen slot that is not in |
| // the member. |
| NormalizedMurmurHashEntropyProvider ComputeRemainderEntropy( |
| const Layer::LayerMember& chosen_member, |
| SlotSelection selection) { |
| ValueInRange slot_of_member = |
| SlotOfMember(chosen_member, selection.slot.value); |
| ValueInRange remainder = |
| CombineRanges(slot_of_member, selection.pseudorandom_remainder); |
| return NormalizedMurmurHashEntropyProvider(remainder); |
| } |
| |
| bool ValidSlotBounds(const Layer& layer_proto) { |
| // Since num_slots divides LES range, we know it is small enough that |
| // num_slots + 1 does not overflow. |
| DCHECK_LE(layer_proto.num_slots(), layer_proto.num_slots() + 1); |
| for (const auto& member : layer_proto.members()) { |
| uint32_t next_slot_after_processed_ranges = 0; |
| for (const auto& range : member.slots()) { |
| // Ranges should be non-overlapping. We also require them to be in |
| // increasing order so that we can easily validate that they are not |
| // overlapping. |
| if (range.start() < next_slot_after_processed_ranges) { |
| return false; |
| } |
| // start and end are both unsigned (uint32_t) so no need to check that |
| // they are non-negative. |
| if (range.end() >= layer_proto.num_slots()) |
| return false; |
| if (range.start() > range.end()) { |
| return false; |
| } |
| next_slot_after_processed_ranges = range.end() + 1; |
| } |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| VariationsLayers::VariationsLayers(const VariationsSeed& seed, |
| const EntropyProviders& entropy_providers) |
| : nil_entropy({0, 1}) { |
| // Don't activate any layer-constrained studies in benchmarking mode to |
| // maintain deterministic behavior. |
| if (entropy_providers.benchmarking_enabled()) |
| return; |
| // TODO(crbug.com/1154033): Support a way to expire old/unused layers so they |
| // no longer get processed by the clients. |
| for (const Layer& layer_proto : seed.layers()) |
| ConstructLayer(entropy_providers, layer_proto); |
| } |
| |
| VariationsLayers::VariationsLayers() : nil_entropy({0, 1}) {} |
| |
| VariationsLayers::~VariationsLayers() = default; |
| |
| void VariationsLayers::ConstructLayer(const EntropyProviders& entropy_providers, |
| const Layer& layer_proto) { |
| if (!layer_proto.unknown_fields().empty()) { |
| LogInvalidLayerReason(InvalidLayerReason::kUnknownFields); |
| return; |
| } |
| if (layer_proto.id() == 0) { |
| LogInvalidLayerReason(InvalidLayerReason::kInvalidId); |
| return; |
| } |
| if (layer_proto.num_slots() == 0) { |
| LogInvalidLayerReason(InvalidLayerReason::kNoSlots); |
| return; |
| } |
| if (layer_proto.members_size() == 0) { |
| LogInvalidLayerReason(InvalidLayerReason::kNoMembers); |
| return; |
| } |
| |
| if (layer_proto.entropy_mode() != Layer::LOW && |
| layer_proto.entropy_mode() != Layer::DEFAULT) { |
| LogInvalidLayerReason(InvalidLayerReason::kInvalidEntropyMode); |
| return; |
| } |
| |
| // Using the size of the domain as the output range maximizes the number of |
| // possible pseudorandom outputs when using the low entropy source. |
| size_t range = entropy_providers.low_entropy_domain(); |
| if (range % layer_proto.num_slots() != 0) { |
| // We can't support uniform selection on layers with a slot count that |
| // doesn't divide the low entropy range, so don't support them at all. |
| LogInvalidLayerReason( |
| InvalidLayerReason::kSlotsDoNotDivideLowEntropyDomain); |
| return; |
| } |
| |
| if (!ValidSlotBounds(layer_proto)) { |
| LogInvalidLayerReason(InvalidLayerReason::kInvalidSlotBounds); |
| return; |
| } |
| |
| const auto& entropy_provider = (layer_proto.entropy_mode() != Layer::LOW) |
| ? entropy_providers.default_entropy() |
| : entropy_providers.low_entropy(); |
| uint32_t salt = layer_proto.salt() ? layer_proto.salt() : layer_proto.id(); |
| ValueInRange pseudorandom = { |
| .value = entropy_provider.GetPseudorandomValue(salt, range), |
| .range = static_cast<uint32_t>(range), |
| }; |
| SlotSelection selection = SelectSlot(pseudorandom, layer_proto.num_slots()); |
| const auto* chosen_member = |
| FindActiveMemberBySlot(selection.slot.value, layer_proto); |
| if (!chosen_member) { |
| // No member is active for the chosen slot. |
| return; |
| } |
| |
| // Store the active member info, along with the remainder entropy. |
| active_member_for_layer_.emplace( |
| layer_proto.id(), LayerInfo{ |
| .active_member_id = chosen_member->id(), |
| .entropy_mode = layer_proto.entropy_mode(), |
| .remainder_entropy = ComputeRemainderEntropy( |
| *chosen_member, selection), |
| }); |
| } |
| |
| bool VariationsLayers::IsLayerMemberActive(uint32_t layer_id, |
| uint32_t member_id) const { |
| auto layer_iter = active_member_for_layer_.find(layer_id); |
| if (layer_iter == active_member_for_layer_.end()) |
| return false; |
| |
| return layer_iter->second.active_member_id && |
| (member_id == layer_iter->second.active_member_id); |
| } |
| |
| bool VariationsLayers::ActiveLayerMemberDependsOnHighEntropy( |
| uint32_t layer_id) const { |
| auto layer_iter = active_member_for_layer_.find(layer_id); |
| if (layer_iter == active_member_for_layer_.end()) |
| return false; |
| |
| return layer_iter->second.entropy_mode == Layer::DEFAULT; |
| } |
| |
| const base::FieldTrial::EntropyProvider& VariationsLayers::GetRemainderEntropy( |
| uint32_t layer_id) const { |
| auto layer_iter = active_member_for_layer_.find(layer_id); |
| if (layer_iter == active_member_for_layer_.end()) { |
| // TODO(holte): Remove CreateTrialsForStudy fuzzer, then uncomment this. |
| // NOTREACHED(); |
| return nil_entropy; |
| } |
| return layer_iter->second.remainder_entropy; |
| } |
| |
| } // namespace variations |