289 lines
11 KiB
C
289 lines
11 KiB
C
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0 OR ISC
|
|
|
|
#include <openssl/base.h>
|
|
|
|
#include "fork_ube_detect.h"
|
|
#include "vm_ube_detect.h"
|
|
|
|
#include "internal.h"
|
|
#include "../internal.h"
|
|
|
|
// What is a uniqueness-breaking event (UBE)?
|
|
// AWS-LC manages state that must be unique on each usage. For example, the
|
|
// CTR-DRBG state must be unique on each usage, otherwise the generation
|
|
// function will produce duplicated values. We name such state "unique state"
|
|
// or "unique memory" if referring directly to memory. Before using a unique
|
|
// state, we must make sure to randomize it to preserve its uniqueness.
|
|
//
|
|
// When/how to randomize state depends on the context. For example, the CTR-DRBG
|
|
// state, that is managed by AWS-LC, will randomize itself before usage during
|
|
// normal operation. But there are events where this assumption is violated. We
|
|
// call such events "uniqueness breaking events" (UBE). Forking an address space
|
|
// is an example of an UBE.
|
|
//
|
|
// By detecting UBE's we can ensure that unique state is properly randomized.
|
|
// AWS-LC is currently able to detect two different type of UBE's that would
|
|
// violate usage requirements of a unique state. The two UBE types are:
|
|
// Forking and VM resuming. Note, however, that detection methods rely on
|
|
// technology that is unique to a platform. Hence, support for UBE detection
|
|
// also depends on the platform AWS-LC is executed on.
|
|
//
|
|
// This file implements and manages the general machinery to detect an UBE. This
|
|
// should be used by the rest of the code-base to implement proper randomization
|
|
// of unique states before usage.
|
|
//
|
|
// ATTENTION: Before attempting to re-write this logic and add more mechanisms
|
|
// review P269907867 carefully.
|
|
|
|
static CRYPTO_once_t ube_state_initialize_once = CRYPTO_ONCE_INIT;
|
|
static CRYPTO_once_t ube_detection_unavailable_once = CRYPTO_ONCE_INIT;
|
|
static struct CRYPTO_STATIC_MUTEX ube_lock = CRYPTO_STATIC_MUTEX_INIT;
|
|
// Locking for testing-specific code. Don't use |ube_lock| to avoid any
|
|
// potential for deadlocks.
|
|
static struct CRYPTO_STATIC_MUTEX ube_testing_lock = CRYPTO_STATIC_MUTEX_INIT;
|
|
static uint8_t ube_detection_unavailable = 0;
|
|
static uint8_t allow_mocked_detection = 0;
|
|
|
|
static uint64_t override_fork_generation_number = 0;
|
|
void set_fork_ube_generation_number_FOR_TESTING(uint64_t fork_gn) {
|
|
CRYPTO_STATIC_MUTEX_lock_write(&ube_testing_lock);
|
|
override_fork_generation_number = fork_gn;
|
|
CRYPTO_STATIC_MUTEX_unlock_write(&ube_testing_lock);
|
|
}
|
|
|
|
static uint32_t override_vm_ube_generation_number = 0;
|
|
void set_vm_ube_generation_number_FOR_TESTING(uint32_t vm_ube_gn) {
|
|
CRYPTO_STATIC_MUTEX_lock_write(&ube_testing_lock);
|
|
override_vm_ube_generation_number = vm_ube_gn;
|
|
CRYPTO_STATIC_MUTEX_unlock_write(&ube_testing_lock);
|
|
}
|
|
|
|
static int get_vm_ube_generation_number(uint32_t *gn) {
|
|
if (allow_mocked_detection == 1) {
|
|
CRYPTO_STATIC_MUTEX_lock_read(&ube_testing_lock);
|
|
*gn = override_vm_ube_generation_number;
|
|
CRYPTO_STATIC_MUTEX_unlock_read(&ube_testing_lock);
|
|
return 1;
|
|
}
|
|
|
|
return CRYPTO_get_vm_ube_generation(gn);
|
|
}
|
|
|
|
static int get_fork_generation_number(uint64_t *gn) {
|
|
if (allow_mocked_detection == 1) {
|
|
CRYPTO_STATIC_MUTEX_lock_read(&ube_testing_lock);
|
|
*gn = override_fork_generation_number;
|
|
CRYPTO_STATIC_MUTEX_unlock_read(&ube_testing_lock);
|
|
return 1;
|
|
}
|
|
|
|
uint64_t fork_gn = CRYPTO_get_fork_ube_generation();
|
|
if (fork_gn == 0) {
|
|
return 0;
|
|
}
|
|
|
|
*gn = fork_gn;
|
|
return 1;
|
|
}
|
|
|
|
|
|
// The UBE generation number is shared for the entire address space. One could
|
|
// implement a per-thread UBE generation number. However, this could be
|
|
// inefficient for global unique states if care is not taken. Because every
|
|
// thread only have visibility on their own per-thread generation number, we
|
|
// could trigger a new randomization of the unique state per-thread. While a
|
|
// single randomization of the global unique state should be sufficient.
|
|
struct ube_state {
|
|
uint64_t generation_number;
|
|
uint64_t cached_fork_gn;
|
|
uint32_t cached_vm_ube_gn;
|
|
};
|
|
static struct ube_state ube_global_state = { 0, 0, 0 };
|
|
|
|
// Convenience object that makes it easier to extend the number of detection
|
|
// methods without having to modify function signatures.
|
|
struct detection_gn {
|
|
#define NUMBER_OF_DETECTION_GENERATION_NUMBERS 2
|
|
uint64_t current_fork_gn;
|
|
uint32_t current_vm_ube_gn;
|
|
};
|
|
|
|
// set_ube_detection_unavailable_once is the single mutation point of
|
|
// |ube_detection_unavailable|. Sets the variable to 1 (true).
|
|
static void set_ube_detection_unavailable_once(void) {
|
|
ube_detection_unavailable = 1;
|
|
}
|
|
|
|
// ube_failed is a convenience function to synchronize mutation of
|
|
// |ube_detection_unavailable|. In practice, synchronization is not strictly
|
|
// needed because currently the mutation is only ever assigning 1 to the
|
|
// variable.
|
|
static void ube_failed(void) {
|
|
CRYPTO_once(&ube_detection_unavailable_once, set_ube_detection_unavailable_once);
|
|
}
|
|
|
|
// ube_state_initialize initializes the global state |ube_global_state|.
|
|
static void ube_state_initialize(void) {
|
|
|
|
ube_global_state.generation_number = 0;
|
|
int ret_fork_gn = get_fork_generation_number(
|
|
&(ube_global_state.cached_fork_gn));
|
|
int ret_vm_ube_gn = get_vm_ube_generation_number(
|
|
&(ube_global_state.cached_vm_ube_gn));
|
|
|
|
if (ret_fork_gn == 0 || ret_vm_ube_gn == 0) {
|
|
ube_failed();
|
|
}
|
|
}
|
|
|
|
// ube_update_state updates the global state |ube_global_state| with the
|
|
// generation numbers loaded into |current_detection_gn| and increments the UBE
|
|
// generation number.
|
|
static void ube_update_state(struct detection_gn *current_detection_gn) {
|
|
|
|
ube_global_state.generation_number += 1;
|
|
|
|
// Make sure we cache all new generation numbers. Otherwise, we might detect
|
|
// a fork UBE but, in fact, both a fork and vm_ube UBE occurred. Then next
|
|
// time we enter, a redundant reseed will be emitted.
|
|
ube_global_state.cached_fork_gn = current_detection_gn->current_fork_gn;
|
|
ube_global_state.cached_vm_ube_gn = current_detection_gn->current_vm_ube_gn;
|
|
}
|
|
|
|
// ube_get_detection_generation_numbers loads the current detection generation
|
|
// numbers into |current_detection_gn|.
|
|
//
|
|
// Returns 1 on success and 0 otherwise. The 0 return value means that a
|
|
// detection method we expected to be available, is in fact not.
|
|
static int ube_get_detection_generation_numbers(
|
|
struct detection_gn *current_detection_gn) {
|
|
|
|
// An attempt to prevent a situation where new detection methods are added but
|
|
// we forget to load them.
|
|
OPENSSL_STATIC_ASSERT(NUMBER_OF_DETECTION_GENERATION_NUMBERS == 2,
|
|
not_handling_the_exact_number_of_detection_generation_numbers);
|
|
|
|
int ret_detect_gn = get_fork_generation_number(
|
|
&(current_detection_gn->current_fork_gn));
|
|
int ret_vm_ube_gn = get_vm_ube_generation_number(
|
|
&(current_detection_gn->current_vm_ube_gn));
|
|
|
|
if (ret_detect_gn == 0 || ret_vm_ube_gn == 0) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
// ube_is_detected computes whether a UBE has been detected or not by comparing
|
|
// the cached detection generation numbers with the current detection generation
|
|
// numbers. The current generation numbers must be loaded into
|
|
// |current_detection_gn| before calling this function.
|
|
//
|
|
// Returns 1 if UBE has been detected and 0 if no UBE has been detected.
|
|
//
|
|
// ATTENTION: Before attempting to re-write this logic and add more mechanisms
|
|
// review P269907867 carefully.
|
|
static int ube_is_detected(struct detection_gn *current_detection_gn) {
|
|
|
|
if (ube_global_state.cached_fork_gn != current_detection_gn->current_fork_gn ||
|
|
ube_global_state.cached_vm_ube_gn != current_detection_gn->current_vm_ube_gn) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int CRYPTO_get_ube_generation_number(uint64_t *current_generation_number) {
|
|
|
|
GUARD_PTR(current_generation_number);
|
|
|
|
CRYPTO_once(&ube_state_initialize_once, ube_state_initialize);
|
|
|
|
// If something failed at an earlier point short-circuit immediately. This
|
|
// saves work in case the UBE detection is not supported. The check below
|
|
// must be done after attempting to initialize the UBE state. Because
|
|
// initialization might fail and we can short-circuit here.
|
|
//
|
|
// |ube_detection_unavailable| and |allow_mocked_detection| are both global
|
|
// variables that can be mutated in multiple threads. They are uint8_t typed
|
|
// though and we assume read and write to them is atomic. This path is hot.
|
|
// The assumption avoids us having to acquire (up to) two read locks that
|
|
// would probably never be contended anyway. That would not block other
|
|
// threads, but cost cycles regardless.
|
|
if (ube_detection_unavailable == 1 &&
|
|
allow_mocked_detection != 1) {
|
|
return 0;
|
|
}
|
|
|
|
struct detection_gn current_detection_gn = { 0, 0 };
|
|
|
|
// First read generation numbers for each supported detection method. We do
|
|
// not mutate |ube_global_state|. So, a read lock is sufficient at this point.
|
|
// Each individual detection method will have their own concurrency controls
|
|
// if needed.
|
|
|
|
if (ube_get_detection_generation_numbers(¤t_detection_gn) != 1) {
|
|
ube_failed();
|
|
return 0;
|
|
}
|
|
CRYPTO_STATIC_MUTEX_lock_read(&ube_lock);
|
|
if (ube_is_detected(¤t_detection_gn) == 0) {
|
|
// No UBE detected, so just grab UBE generation number from the state.
|
|
*current_generation_number = ube_global_state.generation_number;
|
|
CRYPTO_STATIC_MUTEX_unlock_read(&ube_lock);
|
|
return 1;
|
|
}
|
|
CRYPTO_STATIC_MUTEX_unlock_read(&ube_lock);
|
|
|
|
// Reaching this point means that an UBE has been detected. We must now
|
|
// synchronize an update to the UBE generation number. To avoid redundant
|
|
// reseeds, we must ensure the generation number is only incremented once for
|
|
// all UBE's that might have happened. Therefore, first take a write lock but
|
|
// before mutating the state, check for an UBE again. Checking again ensures
|
|
// that only one thread increments the UBE generation number, because the
|
|
// cached detection method generation numbers have been updated by the thread
|
|
// that had the first entry.
|
|
|
|
CRYPTO_STATIC_MUTEX_lock_write(&ube_lock);
|
|
if (ube_get_detection_generation_numbers(¤t_detection_gn) != 1) {
|
|
ube_failed();
|
|
CRYPTO_STATIC_MUTEX_unlock_write(&ube_lock);
|
|
return 0;
|
|
}
|
|
if (ube_is_detected(¤t_detection_gn) == 0) {
|
|
// Another thread already updated the global state. Just load the UBE
|
|
// generation number instead.
|
|
*current_generation_number = ube_global_state.generation_number;
|
|
CRYPTO_STATIC_MUTEX_unlock_write(&ube_lock);
|
|
return 1;
|
|
}
|
|
|
|
// Okay, we are really the first to update the state after detecting an UBE.
|
|
ube_update_state(¤t_detection_gn);
|
|
*current_generation_number = ube_global_state.generation_number;
|
|
CRYPTO_STATIC_MUTEX_unlock_write(&ube_lock);
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Synchronize writing to |allow_mocked_detection|. But only to more easily
|
|
// reason about ordering. They are supposed to only be used in testing code
|
|
// and called, initially, from a single-threaded process at initialization time.
|
|
// We generally don't care about any contention that might happen.
|
|
|
|
void allow_mocked_ube_detection_FOR_TESTING(void) {
|
|
CRYPTO_STATIC_MUTEX_lock_write(&ube_testing_lock);
|
|
allow_mocked_detection = 1;
|
|
CRYPTO_STATIC_MUTEX_unlock_write(&ube_testing_lock);
|
|
}
|
|
|
|
void disable_mocked_ube_detection_FOR_TESTING(void) {
|
|
CRYPTO_STATIC_MUTEX_lock_write(&ube_testing_lock);
|
|
allow_mocked_detection = 0;
|
|
CRYPTO_STATIC_MUTEX_unlock_write(&ube_testing_lock);
|
|
|
|
set_fork_ube_generation_number_FOR_TESTING(0);
|
|
set_vm_ube_generation_number_FOR_TESTING(0);
|
|
}
|