Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,38 @@ More information on this can be found in [INCORPORATING.md](/INCORPORATING.md).
# VM UBE Detection

A VMM can snapshot, clone, and restore VMs. AWS-LC supports VM UBE-type
uniqueness breaking event detection on Linux using SysGenID
(https://lkml.org/lkml/2021/3/8/677). This mechanism is used for security
hardening. If a SysGenID interface is not found, then the mechanism is ignored.
uniqueness breaking event detection on Linux. This mechanism is used for
security hardening. Two backends are supported:

- **vmclock** (preferred) via `/dev/vmclock0`, following the
[vmclock specification](https://uapi-group.org/specifications/specs/vmclock/).
- **SysGenID** (fallback) via `/dev/sysgenid`
(https://lkml.org/lkml/2021/3/8/677).

At initialization AWS-LC prefers vmclock: if `/dev/vmclock0` is present and
usable it is used, otherwise AWS-LC falls back to `/dev/sysgenid`. If neither
interface is found, the mechanism is ignored.

## VM UBE Prerequisites

VM snapshots taken on active hosts can potentially be unsafe to use.
See "Snapshot Safety Prerequisites" here: https://lkml.org/lkml/2021/3/8/677

## Testing VM UBE Detection

The device nodes are not generally available in build/test environments, so
each backend can be pointed at a regular file that stands in for its device
node:

- `-DTEST_SYSGENID_PATH=<file>` exercises the SysGenID backend.
- `-DTEST_VMCLOCK_PATH=<file>` exercises the vmclock backend.

Setting either option enables VM UBE test mode. Because vmclock is preferred at
runtime, enabling both in a single build only exercises vmclock; to exercise the
SysGenID path (including the vmclock-to-SysGenID fallback), build with
`-DTEST_SYSGENID_PATH` alone. The CI scripts under `tests/ci/` build each backend
in a separate configuration for this reason.

# FIPS Mode

For more details on building AWS-LC in FIPS mode, see the [crypto/fipsmodule/FIPS.md](crypto/fipsmodule/FIPS.md).
Expand Down
26 changes: 23 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,31 @@ install(DIRECTORY include/openssl
PATTERN "*.in" EXCLUDE
)

if (TEST_SYSGENID_PATH)
# VM UBE (Uniqueness Breaking Event) detection can be exercised in tests by
# pointing a backend at a regular file that stands in for its device node.
# TEST_SYSGENID_PATH and TEST_VMCLOCK_PATH are independent: setting either one
# enables VM UBE test mode (AWSLC_VM_UBE_TESTING) and additionally sets a
# per-backend flag (AWSLC_TEST_SYSGENID / AWSLC_TEST_VMCLOCK) so that only the
# backend actually under test has its stand-in file created at startup. A build
# that enables only one backend must NOT try to create the other's file, which
# would point at the real /dev node and fail.
#
# Because vmclock is preferred over sysgenid at runtime, enabling both in a
# single build only exercises vmclock. To exercise the sysgenid path (including
# the vmclock->sysgenid fallback), build with TEST_SYSGENID_PATH alone.
if (TEST_SYSGENID_PATH OR TEST_VMCLOCK_PATH)
message(STATUS "Setting AWSLC_VM_UBE_TESTING=1")
add_definitions(-DAWSLC_VM_UBE_TESTING=1)
message(STATUS "Setting AWSLC_SYSGENID_PATH=${TEST_SYSGENID_PATH}")
add_definitions(-DAWSLC_SYSGENID_PATH=\"${TEST_SYSGENID_PATH}\")
if (TEST_SYSGENID_PATH)
message(STATUS "Setting AWSLC_SYSGENID_PATH=${TEST_SYSGENID_PATH}")
add_definitions(-DAWSLC_SYSGENID_PATH=\"${TEST_SYSGENID_PATH}\")
add_definitions(-DAWSLC_TEST_SYSGENID=1)
endif()
if (TEST_VMCLOCK_PATH)
message(STATUS "Setting AWSLC_VMCLOCK_PATH=${TEST_VMCLOCK_PATH}")
add_definitions(-DAWSLC_VMCLOCK_PATH=\"${TEST_VMCLOCK_PATH}\")
add_definitions(-DAWSLC_TEST_VMCLOCK=1)
endif()
endif()

if(NOT DISABLE_PERL)
Expand Down
2 changes: 1 addition & 1 deletion crypto/fipsmodule/rand/rand_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ TEST_F(randTest, UbeDetectionMocked) {

MockedUbeDetection(
[](uint64_t gn) {
set_vm_ube_generation_number_FOR_TESTING(static_cast<uint32_t>(gn));
set_vm_ube_generation_number_FOR_TESTING(gn);
}
);
}
Expand Down
1 change: 1 addition & 0 deletions crypto/libcrypto.map
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ AWS_LC_1.0 {
CRYPTO_get_vm_ube_active;
CRYPTO_get_vm_ube_generation;
CRYPTO_get_vm_ube_supported;
CRYPTO_get_vmclock_path;
CRYPTO_has_asm;
CRYPTO_has_broken_NEON;
CRYPTO_is_ARMv8_DIT_capable_for_testing;
Expand Down
1 change: 1 addition & 0 deletions crypto/libcrypto.txt
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ CRYPTO_get_ube_generation_number AWS_LC_1.0 PRIVATE
CRYPTO_get_vm_ube_active AWS_LC_1.0 PRIVATE
CRYPTO_get_vm_ube_generation AWS_LC_1.0 PRIVATE
CRYPTO_get_vm_ube_supported AWS_LC_1.0 PRIVATE
CRYPTO_get_vmclock_path AWS_LC_1.0 PRIVATE
CRYPTO_has_asm AWS_LC_1.0 PUBLIC
CRYPTO_has_broken_NEON AWS_LC_1.0 PUBLIC
CRYPTO_is_ARMv8_DIT_capable_for_testing AWS_LC_1.0 PRIVATE
Expand Down
3 changes: 2 additions & 1 deletion crypto/rand_extra/urandom_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ static void GetTrace(std::vector<Event> *out_trace, unsigned flags,
break;
}

if (filename != CRYPTO_get_sysgenid_path()) {
if (filename != CRYPTO_get_sysgenid_path() &&
filename != CRYPTO_get_vmclock_path()) {
out_trace->push_back(Event::Open(filename));
}

Expand Down
7 changes: 6 additions & 1 deletion crypto/test/gtest_main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@


int main(int argc, char **argv) {
#if defined(OPENSSL_LINUX) && defined(AWSLC_VM_UBE_TESTING)
#if defined(OPENSSL_LINUX) && defined(AWSLC_TEST_SYSGENID)
if (1 != HAZMAT_init_sysgenid_file()) {
abort();
}
#endif
#if defined(OPENSSL_LINUX) && defined(AWSLC_TEST_VMCLOCK)
if (1 != HAZMAT_init_vmclock_file()) {
abort();
}
#endif

testing::InitGoogleTest(&argc, argv);
bssl::SetupGoogleTest();
Expand Down
2 changes: 1 addition & 1 deletion crypto/ube/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ OPENSSL_EXPORT void set_fork_ube_generation_number_FOR_TESTING(uint64_t fork_gn)
// vm_ube detection.
// |allow_mocked_ube_detection_FOR_TESTING| must have been invoked (once
// per-process) to allow mocking the vm_ube generation number.
OPENSSL_EXPORT void set_vm_ube_generation_number_FOR_TESTING(uint32_t vm_ube_gn);
OPENSSL_EXPORT void set_vm_ube_generation_number_FOR_TESTING(uint64_t vm_ube_gn);

// allow_mocked_ube_detection_FOR_TESTING allows mocking UBE detection even
// though real detection is not available. This function must be called in
Expand Down
54 changes: 45 additions & 9 deletions crypto/ube/ube.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ void set_fork_ube_generation_number_FOR_TESTING(uint64_t 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) {
static uint64_t override_vm_ube_generation_number = 0;
void set_vm_ube_generation_number_FOR_TESTING(uint64_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) {
static int get_vm_ube_generation_number(uint64_t *gn) {
if (allow_mocked_detection == 1) {
CRYPTO_STATIC_MUTEX_lock_read(&ube_testing_lock);
*gn = override_vm_ube_generation_number;
Expand Down Expand Up @@ -97,7 +97,7 @@ static int get_fork_generation_number(uint64_t *gn) {
struct ube_state {
uint64_t generation_number;
uint64_t cached_fork_gn;
uint32_t cached_vm_ube_gn;
uint64_t cached_vm_ube_gn;
};
static struct ube_state ube_global_state = { 0, 0, 0 };

Expand All @@ -106,7 +106,7 @@ static struct ube_state ube_global_state = { 0, 0, 0 };
struct detection_gn {
#define NUMBER_OF_DETECTION_GENERATION_NUMBERS 2
uint64_t current_fork_gn;
uint32_t current_vm_ube_gn;
uint64_t current_vm_ube_gn;
};

// set_ube_detection_unavailable_once is the single mutation point of
Expand All @@ -132,6 +132,10 @@ static void ube_state_initialize(void) {
int ret_vm_ube_gn = get_vm_ube_generation_number(
&(ube_global_state.cached_vm_ube_gn));

// Only a permanent failure (0) disables detection. A transient VM UBE read
// failure (-1) here just leaves the cached generation number at 0; the next
// successful read will differ and be treated as a UBE, forcing a reseed --
// conservative and correct, without disabling detection for the process.
if (ret_fork_gn == 0 || ret_vm_ube_gn == 0) {
ube_failed();
}
Expand All @@ -154,8 +158,14 @@ static void ube_update_state(struct detection_gn *current_detection_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.
// Returns tri-state:
// 1 Success.
// 0 Permanent failure: a detection method we expected to be available is in
// fact not. The caller must disable detection (|ube_failed|).
// -1 Transient failure: a method initialized successfully but could not
// produce a consistent read this call (e.g. a momentarily wedged vmclock
// seqlock). The caller must reseed conservatively for this call but must
// NOT disable detection.
static int ube_get_detection_generation_numbers(
struct detection_gn *current_detection_gn) {

Expand All @@ -169,10 +179,19 @@ static int ube_get_detection_generation_numbers(
int ret_vm_ube_gn = get_vm_ube_generation_number(
&(current_detection_gn->current_vm_ube_gn));

// A permanent failure of any method takes precedence: detection is no longer
// trustworthy and must be disabled by the caller.
if (ret_detect_gn == 0 || ret_vm_ube_gn == 0) {
return 0;
}

// Otherwise, a transient VM UBE read failure (-1) means we could not read a
// consistent value this call. Signal a conservative reseed without disabling
// detection.
if (ret_vm_ube_gn == -1) {
return -1;
}

return 1;
}

Expand Down Expand Up @@ -223,10 +242,20 @@ int CRYPTO_get_ube_generation_number(uint64_t *current_generation_number) {
// Each individual detection method will have their own concurrency controls
// if needed.

if (ube_get_detection_generation_numbers(&current_detection_gn) != 1) {
int ret_gn = ube_get_detection_generation_numbers(&current_detection_gn);
if (ret_gn == 0) {
// Permanent failure: a detection method we expected is gone. Disable
// detection for the process and force a conservative reseed.
ube_failed();
return 0;
}
if (ret_gn == -1) {
// Transient read failure (e.g. a momentarily wedged vmclock seqlock). Force
// a conservative reseed for this call, but do NOT disable detection --
// |ube_failed| is CRYPTO_once-guarded and would irreversibly turn off all
// UBE detection (including fork) for the entire process.
return 0;
}
CRYPTO_STATIC_MUTEX_lock_read(&ube_lock);
if (ube_is_detected(&current_detection_gn) == 0) {
// No UBE detected, so just grab UBE generation number from the state.
Expand All @@ -246,11 +275,18 @@ int CRYPTO_get_ube_generation_number(uint64_t *current_generation_number) {
// that had the first entry.

CRYPTO_STATIC_MUTEX_lock_write(&ube_lock);
if (ube_get_detection_generation_numbers(&current_detection_gn) != 1) {
ret_gn = ube_get_detection_generation_numbers(&current_detection_gn);
if (ret_gn == 0) {
ube_failed();
CRYPTO_STATIC_MUTEX_unlock_write(&ube_lock);
return 0;
}
if (ret_gn == -1) {
// Transient read failure: reseed conservatively without disabling
// detection (see the first call site above).
CRYPTO_STATIC_MUTEX_unlock_write(&ube_lock);
return 0;
}
if (ube_is_detected(&current_detection_gn) == 0) {
// Another thread already updated the global state. Just load the UBE
// generation number instead.
Expand Down
86 changes: 73 additions & 13 deletions crypto/ube/ube_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,19 @@ TEST_F(ubeGenerationNumberTest, BasicTests) {
}

static void MockedDetectionMethodTest(
std::function<void(uint32_t)> set_method_generation_number) {
std::function<void(uint64_t)> set_method_generation_number) {

uint64_t generation_number = 0;
uint64_t cached_generation_number = 0;
uint32_t mocked_generation_number = 0;
uint64_t mocked_generation_number = 0;

uint8_t initial_mocked_generation_number[4] = {0};
ASSERT_TRUE(RAND_bytes(initial_mocked_generation_number, 4));
mocked_generation_number =
((uint32_t)initial_mocked_generation_number[0] << 24) |
((uint32_t)initial_mocked_generation_number[1] << 16) |
((uint32_t)initial_mocked_generation_number[2] << 8) |
((uint32_t)initial_mocked_generation_number[3]);
((uint64_t)initial_mocked_generation_number[0] << 24) |
((uint64_t)initial_mocked_generation_number[1] << 16) |
((uint64_t)initial_mocked_generation_number[2] << 8) |
((uint64_t)initial_mocked_generation_number[3]);

// Testing that UBE generation number is incremented when:
// mocked_generation_number + 1
Expand Down Expand Up @@ -133,32 +133,92 @@ TEST_F(ubeGenerationNumberTest, MockedDetectionMethodTests) {
allowMockedUbe();

MockedDetectionMethodTest(
[](uint32_t gn) {
set_fork_ube_generation_number_FOR_TESTING(static_cast<uint64_t>(gn));
[](uint64_t gn) {
set_fork_ube_generation_number_FOR_TESTING(gn);
}
);

MockedDetectionMethodTest(
[](uint32_t gn) {
[](uint64_t gn) {
set_vm_ube_generation_number_FOR_TESTING(gn);
}
);

MockedDetectionMethodTest(
[](uint32_t gn) {
set_fork_ube_generation_number_FOR_TESTING(static_cast<uint64_t>(gn));
[](uint64_t gn) {
set_fork_ube_generation_number_FOR_TESTING(gn);
set_vm_ube_generation_number_FOR_TESTING(gn);
}
);

MockedDetectionMethodTest(
[](uint32_t gn) {
set_fork_ube_generation_number_FOR_TESTING(static_cast<uint64_t>(gn));
[](uint64_t gn) {
set_fork_ube_generation_number_FOR_TESTING(gn);
set_vm_ube_generation_number_FOR_TESTING(gn + 1);
}
);
}

// Exercises the vm_ube generation number across the full 64-bit range. vmclock
// exposes a 64-bit vm_generation_counter (unlike the legacy 32-bit sysgenid),
// so the orchestration layer must detect changes in the high 32 bits and in
// values that exceed 2^32. |MockedDetectionMethodTest| above only covers a
// 32-bit-range value, so this guards the widening end-to-end.
TEST_F(ubeGenerationNumberTest, MockedVmUbe64BitValues) {
allowMockedUbe();

// A sequence of distinct 64-bit values. Consecutive entries differ only in
// the high 32 bits, only in the low 32 bits, or wrap across the 2^32
// boundary -- each transition must be detected as exactly one UBE.
const uint64_t values[] = {
0x0000000000000001ULL,
0x0000000100000001ULL, // high half changed, low half identical
0x0000000100000002ULL, // low half changed, high half identical
0x00000000FFFFFFFFULL, // drop below 2^32
0x0000000100000000ULL, // cross the 2^32 boundary
0xFFFFFFFFFFFFFFFFULL, // all bits set
0x8000000000000000ULL, // high bit only
};

uint64_t generation_number = 0;
set_vm_ube_generation_number_FOR_TESTING(values[0]);
ASSERT_TRUE(CRYPTO_get_ube_generation_number(&generation_number));

for (size_t i = 1; i < sizeof(values) / sizeof(values[0]); i++) {
uint64_t before = generation_number;

// Changing the mocked vm_ube generation number must bump the UBE
// generation number exactly once.
set_vm_ube_generation_number_FOR_TESTING(values[i]);
generation_number = 0;
ASSERT_TRUE(CRYPTO_get_ube_generation_number(&generation_number));
ASSERT_EQ(generation_number, before + 1) << "at index " << i;

// Stable when the value does not change.
uint64_t stable = 0;
ASSERT_TRUE(CRYPTO_get_ube_generation_number(&stable));
ASSERT_EQ(stable, generation_number) << "instability at index " << i;
}
}

// A change confined entirely to the high 32 bits of the vm_ube generation
// number must still be detected. A 32-bit-truncating implementation would miss
// this (both values alias to 0 in the low 32 bits) and fail to reseed.
TEST_F(ubeGenerationNumberTest, MockedVmUbeHighBitsOnlyChange) {
allowMockedUbe();

uint64_t generation_number = 0;
set_vm_ube_generation_number_FOR_TESTING(0x0000000000000000ULL + 0x100000000ULL);
ASSERT_TRUE(CRYPTO_get_ube_generation_number(&generation_number));

uint64_t before = generation_number;
// Low 32 bits stay 0; only the high 32 bits differ.
set_vm_ube_generation_number_FOR_TESTING(0x200000000ULL);
generation_number = 0;
ASSERT_TRUE(CRYPTO_get_ube_generation_number(&generation_number));
ASSERT_EQ(generation_number, before + 1);
}

TEST_F(ubeGenerationNumberTest, ExpectedSupportTests) {
uint64_t generation_number = 0;
// Operating systems where we expect UBE detection to be enabled.
Expand Down
Loading
Loading