| // Copyright lowRISC contributors. | |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| // SPDX-License-Identifier: Apache-2.0 | |
| #include "sw/device/lib/base/hardened_memory.h" | |
| #include "sw/device/lib/base/hardened.h" | |
| #include "sw/device/lib/base/memory.h" | |
| #include "sw/device/lib/base/random_order.h" | |
| // NOTE: The three hardened_mem* functions have similar contents, but the parts | |
| // that are shared between them are commented only in `memcpy()`. | |
| void hardened_memcpy(uint32_t *restrict dest, const uint32_t *restrict src, | |
| size_t word_len) { | |
| random_order_t order; | |
| random_order_init(&order, word_len); | |
| size_t count = 0; | |
| size_t expected_count = random_order_len(&order); | |
| // Immediately convert `src` and `dest` to addresses, which erases their | |
| // provenance and causes their addresses to be exposed (in the provenance | |
| // sense). | |
| uintptr_t src_addr = (uintptr_t)src; | |
| uintptr_t dest_addr = (uintptr_t)dest; | |
| // `decoys` is a small stack array that is filled with uninitialized memory. | |
| // It is scratch space for us to do "extra" operations, when the number of | |
| // iteration indices the chosen random order is different from `word_len`. | |
| // | |
| // These extra operations also introduce noise that an attacker must do work | |
| // to filter, such as by applying side-channel analysis to obtain an address | |
| // trace. | |
| uint32_t decoys[8]; | |
| uintptr_t decoy_addr = (uintptr_t)&decoys; | |
| // We need to launder `count`, so that the SW.LOOP-COMPLETION check is not | |
| // deleted by the compiler. | |
| size_t byte_len = word_len * sizeof(uint32_t); | |
| for (; launderw(count) < expected_count; count = launderw(count) + 1) { | |
| // The order values themselves are in units of words, but we need `byte_idx` | |
| // to be in units of bytes. | |
| // | |
| // The value obtained from `advance()` is laundered, to prevent | |
| // implementation details from leaking across procedures. | |
| size_t byte_idx = launderw(random_order_advance(&order)) * sizeof(uint32_t); | |
| // Prevent the compiler from reordering the loop; this ensures a | |
| // happens-before among indices consistent with `order`. | |
| barrierw(byte_idx); | |
| // Compute putative offsets into `src`, `dest`, and `decoys`. Some of these | |
| // may go off the end of `src` and `dest`, but they will not be cast to | |
| // pointers in that case. (Note that casting out-of-range addresses to | |
| // pointers is UB.) | |
| uintptr_t srcp = src_addr + byte_idx; | |
| uintptr_t destp = dest_addr + byte_idx; | |
| uintptr_t decoy1 = decoy_addr + (byte_idx % sizeof(decoys)); | |
| uintptr_t decoy2 = | |
| decoy_addr + ((byte_idx + sizeof(decoys) / 2) % sizeof(decoys)); | |
| // Branchlessly select whether to do a "real" copy or a decoy copy, | |
| // depending on whether we've gone off the end of the array or not. | |
| // | |
| // Pretty much everything needs to be laundered: we need to launder | |
| // `byte_idx` for obvious reasons, and we need to launder the result of the | |
| // select, so that the compiler cannot delete the resulting loads and | |
| // stores. This is similar to having used `volatile uint32_t *`. | |
| void *src = (void *)launderw( | |
| ct_cmovw(ct_sltuw(launderw(byte_idx), byte_len), srcp, decoy1)); | |
| void *dest = (void *)launderw( | |
| ct_cmovw(ct_sltuw(launderw(byte_idx), byte_len), destp, decoy2)); | |
| // Perform the copy, without performing a typed dereference operation. | |
| write_32(read_32(src), dest); | |
| } | |
| HARDENED_CHECK_EQ(count, expected_count); | |
| } | |
| // The source of randomness for shred, which may be replaced at link-time. | |
| OT_WEAK | |
| uint32_t hardened_memshred_random_word(void) { return 0xcaffe17e; } | |
| void hardened_memshred(uint32_t *dest, size_t word_len) { | |
| random_order_t order; | |
| random_order_init(&order, word_len); | |
| size_t count = 0; | |
| size_t expected_count = random_order_len(&order); | |
| uintptr_t data_addr = (uintptr_t)dest; | |
| uint32_t decoys[8]; | |
| uintptr_t decoy_addr = (uintptr_t)&decoys; | |
| size_t byte_len = word_len * sizeof(uint32_t); | |
| for (; count < expected_count; count = launderw(count) + 1) { | |
| size_t byte_idx = launderw(random_order_advance(&order)) * sizeof(uint32_t); | |
| barrierw(byte_idx); | |
| uintptr_t datap = data_addr + byte_idx; | |
| uintptr_t decoy = decoy_addr + (byte_idx % sizeof(decoys)); | |
| void *data = (void *)launderw( | |
| ct_cmovw(ct_sltuw(launderw(byte_idx), byte_len), datap, decoy)); | |
| // Write a freshly-generated random word to `*data`. | |
| write_32(hardened_memshred_random_word(), data); | |
| } | |
| HARDENED_CHECK_EQ(count, expected_count); | |
| } | |
| hardened_bool_t hardened_memeq(const uint32_t *lhs, const uint32_t *rhs, | |
| size_t word_len) { | |
| random_order_t order; | |
| random_order_init(&order, word_len); | |
| size_t count = 0; | |
| size_t expected_count = random_order_len(&order); | |
| uintptr_t lhs_addr = (uintptr_t)lhs; | |
| uintptr_t rhs_addr = (uintptr_t)rhs; | |
| // `decoys` needs to be filled with equal values this time around. It | |
| // should be filled with values with a Hamming weight of around 16, which is | |
| // the most common hamming weight among 32-bit words. | |
| uint32_t decoys[8] = { | |
| 0xaaaaaaaa, 0xaaaaaaaa, 0xaaaaaaaa, 0xaaaaaaaa, | |
| 0xaaaaaaaa, 0xaaaaaaaa, 0xaaaaaaaa, 0xaaaaaaaa, | |
| }; | |
| uintptr_t decoy_addr = (uintptr_t)&decoys; | |
| uint32_t zeros = 0; | |
| uint32_t ones = UINT32_MAX; | |
| // The loop is almost token-for-token the one above, but the copy is | |
| // replaced with something else. | |
| size_t byte_len = word_len * sizeof(uint32_t); | |
| for (; count < expected_count; count = launderw(count) + 1) { | |
| size_t byte_idx = launderw(random_order_advance(&order)) * sizeof(uint32_t); | |
| barrierw(byte_idx); | |
| uintptr_t ap = lhs_addr + byte_idx; | |
| uintptr_t bp = rhs_addr + byte_idx; | |
| uintptr_t decoy1 = decoy_addr + (byte_idx % sizeof(decoys)); | |
| uintptr_t decoy2 = | |
| decoy_addr + ((byte_idx + sizeof(decoys) / 2) % sizeof(decoys)); | |
| void *av = (void *)launderw( | |
| ct_cmovw(ct_sltuw(launderw(byte_idx), byte_len), ap, decoy1)); | |
| void *bv = (void *)launderw( | |
| ct_cmovw(ct_sltuw(launderw(byte_idx), byte_len), bp, decoy2)); | |
| uint32_t a = read_32(av); | |
| uint32_t b = read_32(bv); | |
| // Launder one of the operands, so that the compiler cannot cache the result | |
| // of the xor for use in the next operation. | |
| // | |
| // We launder `zeroes` so that compiler cannot learn that `zeroes` has | |
| // strictly more bits set at the end of the loop. | |
| zeros = launder32(zeros) | (launder32(a) ^ b); | |
| // Same as above. The compiler can cache the value of `a[offset]`, but it | |
| // has no chance to strength-reduce this operation. | |
| ones = launder32(ones) & (launder32(a) ^ ~b); | |
| } | |
| HARDENED_CHECK_EQ(count, expected_count); | |
| if (launder32(zeros) == 0) { | |
| HARDENED_CHECK_EQ(ones, UINT32_MAX); | |
| return kHardenedBoolTrue; | |
| } | |
| HARDENED_CHECK_NE(ones, UINT32_MAX); | |
| return kHardenedBoolFalse; | |
| } |