From 04734235380c371f184ee1bc136954a818152012 Mon Sep 17 00:00:00 2001 From: Sonali Saha Date: Fri, 27 Sep 2024 16:25:00 +0530 Subject: [PATCH] [LibOS] Add Page Cache feature for trusted files The Page Cache feature for trusted files has been implemented using a Least Recently Used (LRU) eviction policy. Files that are accessed more than 10 times will be cached to enhance performance. Additionally, a new manifest option `sgx.trusted_files_cache_size` has been introduced to specify the size of the cache allocated for trusted files. Signed-off-by: Sonali Saha --- CI-Examples/nginx/nginx.manifest.template | 4 + Documentation/manifest-syntax.rst | 35 ++++ Documentation/performance.rst | 13 ++ libos/include/libos_fs.h | 5 +- libos/include/lru_composite_key_cache.h | 32 +++ libos/src/fs/chroot/fs.c | 2 +- libos/src/fs/chroot/lru_composite_key_cache.c | 188 ++++++++++++++++++ libos/src/fs/chroot/trusted.c | 142 ++++++++++++- libos/src/meson.build | 1 + python/graminelibos/manifest_check.py | 1 + 10 files changed, 416 insertions(+), 7 deletions(-) create mode 100644 libos/include/lru_composite_key_cache.h create mode 100644 libos/src/fs/chroot/lru_composite_key_cache.c diff --git a/CI-Examples/nginx/nginx.manifest.template b/CI-Examples/nginx/nginx.manifest.template index b25ebc0463..8a996e93a6 100644 --- a/CI-Examples/nginx/nginx.manifest.template +++ b/CI-Examples/nginx/nginx.manifest.template @@ -51,3 +51,7 @@ sgx.trusted_files = [ sgx.allowed_files = [ "file:{{ install_dir }}/logs", ] + +# Trusted files cache of 16k size is enough to cover common Nginx static HTTP file of 10k size +# which will be cached by Gramine. +sgx.trusted_files_cache_size = "16K" diff --git a/Documentation/manifest-syntax.rst b/Documentation/manifest-syntax.rst index c531139ff0..db8fc385c2 100644 --- a/Documentation/manifest-syntax.rst +++ b/Documentation/manifest-syntax.rst @@ -1080,6 +1080,41 @@ Marking files as trusted is especially useful for shared libraries: a |~| trusted library cannot be silently replaced by a malicious host because the hash verification will fail. +.. _trusted-files-cache-size: + +Trusted files cache size +^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + sgx.trusted_files_cache_size = [NUM] + (Default: 0) + +This syntax specifies the size of the cache allocated for trusted files. By +default, this optimization is turned off because the optimal cache size can +differ based on the specific requirements of the application. Units like +``K`` |~| (KiB), ``M`` |~| (MiB), and ``G`` |~| (GiB) can be appended to the +values for convenience. For example, ``sgx.trusted_files_cache_size = "16K"`` +indicates a 16 |~| KiB trusted files cache size. + +When enabled, the cache is designed to store files that are accessed +frequently. Specifically, only files that are opened more than 10 times will +be cached. This approach ensures that files with higher access frequencies +benefit from being stored in memory, thereby improving performance. + +The cache utilizes a Least Recently Used (LRU) eviction policy. Under this +policy, when the cache reaches its capacity, the file chunks that have been +accessed the least recently are removed to make space for new file chunks. +This method helps maintain the most frequently accessed file chunks in the +cache while discarding those that have not been used recently. + +This optimization is particularly advantageous for applications dealing with +opening files that are accessed repeatedly. By enabling the cache, +the overhead of repeatedly opening and reading these files is reduced, +as the files are kept in memory for quicker access. However, if files are +infrequently used, enabling this cache might not provide significant +performance benefits and could consume unnecessary memory resources. + .. _encrypted-files: Encrypted files diff --git a/Documentation/performance.rst b/Documentation/performance.rst index 6cbb4206d0..ddc8b941c1 100644 --- a/Documentation/performance.rst +++ b/Documentation/performance.rst @@ -300,6 +300,19 @@ in Gramine: all IPC is transparently encrypted/decrypted using the TLS-PSK with AES-GCM crypto. +Cache optimization for trusted files +------------------------------------ + +As the trusted file is read, it is split into chunks, and we compute SHA256 +hash for each chunk. Gramine doesn't have an optimization of keeping the +trusted file's content in enclave memory, which implies re-reading and +re-hashing the same file contents every time the file is read at the same +offset. To address this performance bottleneck, instead of loading file chunks +in the enclave each time the file is read, the file chunks are kept in cache. +This optimization is by default disabled, but can be enabled and tuned +according to the needs of the application via the manifest option +``sgx.trusted_files_cache_size``. + .. _choice_of_sgx_machine: Choice of SGX machine diff --git a/libos/include/libos_fs.h b/libos/include/libos_fs.h index db750ae5c7..bd1927124c 100644 --- a/libos/include/libos_fs.h +++ b/libos/include/libos_fs.h @@ -40,8 +40,9 @@ struct allowed_file* get_allowed_file(const char* path); size_t get_chunk_hashes_size(size_t file_size); int load_trusted_file(struct trusted_file* tf, size_t file_size, struct trusted_chunk_hash** out_chunk_hashes); -int read_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t count, uint8_t* buf, - size_t file_size, struct trusted_chunk_hash* chunk_hashes); +int read_and_verify_trusted_file(struct libos_handle* hdl, uint64_t offset, size_t count, + uint8_t* buf, size_t file_size, + struct trusted_chunk_hash* chunk_hashes); int register_allowed_file(const char* path); int init_trusted_files(void); int init_allowed_files(void); diff --git a/libos/include/lru_composite_key_cache.h b/libos/include/lru_composite_key_cache.h new file mode 100644 index 0000000000..4a30a9bbbe --- /dev/null +++ b/libos/include/lru_composite_key_cache.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* Copyright (C) 2024 Intel Corporation */ + +/* Least-recently used cache with composite key, used by the trusted file implementation + for optimizing data access */ + +#pragma once + +#include +#include +#include + +struct lruc_composite_key { + uint64_t id; + uint64_t chunk_number; +}; + +struct lruc_composite_key_context; + +struct lruc_composite_key_context* lruc_composite_key_create(void); +void lruc_composite_key_destroy(struct lruc_composite_key_context* context); +bool lruc_composite_key_add(struct lruc_composite_key_context* context, + struct lruc_composite_key* key, void* data); +void* lruc_composite_key_get(struct lruc_composite_key_context* context, + struct lruc_composite_key* key); +void* lruc_composite_key_find(struct lruc_composite_key_context* context, + struct lruc_composite_key* key); +size_t lruc_composite_key_size(struct lruc_composite_key_context* context); +void* lruc_composite_key_get_first(struct lruc_composite_key_context* context); +void* lruc_composite_key_get_next(struct lruc_composite_key_context* context); +void* lruc_composite_key_get_last(struct lruc_composite_key_context* context); +void lruc_composite_key_remove_last(struct lruc_composite_key_context* context); diff --git a/libos/src/fs/chroot/fs.c b/libos/src/fs/chroot/fs.c index 7cb0800314..2782d2627f 100644 --- a/libos/src/fs/chroot/fs.c +++ b/libos/src/fs/chroot/fs.c @@ -563,7 +563,7 @@ static ssize_t chroot_read(struct libos_handle* hdl, void* buf, size_t count, fi if (is_trusted_from_inode_data(hdl->inode)) { struct chroot_inode_data* data = hdl->inode->data; - ret = read_and_verify_trusted_file(hdl->pal_handle, offset, count, buf, + ret = read_and_verify_trusted_file(hdl, offset, count, buf, hdl->inode->size, data->chunk_hashes); if (ret < 0) return ret; diff --git a/libos/src/fs/chroot/lru_composite_key_cache.c b/libos/src/fs/chroot/lru_composite_key_cache.c new file mode 100644 index 0000000000..346541ab63 --- /dev/null +++ b/libos/src/fs/chroot/lru_composite_key_cache.c @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* Copyright (C) 2024 Intel Corporation */ + +#include "assert.h" +#include "list.h" +#include "lru_composite_key_cache.h" + +#ifdef IN_TOOLS + +#include +#include + +#define uthash_fatal(msg) \ + do { \ + fprintf(stderr, "uthash error: %s\n", msg); \ + exit(-1); \ + } while(0) + +#else + +#include "api.h" + +#define uthash_fatal(msg) \ + do { \ + log_error("uthash error: %s", msg); \ + abort(); \ + } while(0) + +#endif + +#include "uthash.h" + +DEFINE_LIST(lruc_composite_key_list_node); +struct lruc_composite_key_list_node { + LIST_TYPE(lruc_composite_key_list_node) list; + struct lruc_composite_key key; +}; +DEFINE_LISTP(lruc_composite_key_list_node); + +struct lruc_composite_key_map_node { + struct lruc_composite_key key; + void* data; + struct lruc_composite_key_list_node* list_ptr; + UT_hash_handle hh; +}; + +struct lruc_composite_key_context { + /* list and map both contain the same objects (list contains keys, map contains actual data). + * They're kept in sync so that map is used for fast lookups and list is used for fast LRU. + */ + LISTP_TYPE(lruc_composite_key_list_node) list; + struct lruc_composite_key_map_node* map; + struct lruc_composite_key_list_node* current; /* current head of the cache */ +}; + +struct lruc_composite_key_context* lruc_composite_key_create(void) { + struct lruc_composite_key_context* lruc = calloc(1, sizeof(*lruc)); + if (!lruc) + return NULL; + + INIT_LISTP(&lruc->list); + lruc->map = NULL; + lruc->current = NULL; + return lruc; +}; + +static struct lruc_composite_key_map_node* get_map_node(struct lruc_composite_key_context* lruc, + struct lruc_composite_key key) { + struct lruc_composite_key_map_node* mn = NULL; + HASH_FIND(hh, lruc->map, &key, sizeof(struct lruc_composite_key), mn); + return mn; +} + +void lruc_composite_key_destroy(struct lruc_composite_key_context* lruc) { + struct lruc_composite_key_list_node* ln; + struct lruc_composite_key_list_node* tmp; + struct lruc_composite_key_map_node* mn; + + LISTP_FOR_EACH_ENTRY_SAFE(ln, tmp, &lruc->list, list) { + mn = get_map_node(lruc, ln->key); + if (mn) { + HASH_DEL(lruc->map, mn); + free(mn); + } + LISTP_DEL(ln, &lruc->list, list); + free(ln); + } + + assert(LISTP_EMPTY(&lruc->list)); + assert(HASH_COUNT(lruc->map) == 0); + free(lruc); +} + +bool lruc_composite_key_add(struct lruc_composite_key_context* lruc, + struct lruc_composite_key* key, void* data) { + if (get_map_node(lruc, *key)) + return false; + + struct lruc_composite_key_map_node* map_node = calloc(1, sizeof(*map_node)); + if (!map_node) + return false; + + struct lruc_composite_key_list_node* list_node = calloc(1, sizeof(*list_node)); + if (!list_node) { + free(map_node); + return false; + } + + list_node->key = *key; + map_node->key = *key; + LISTP_ADD(list_node, &lruc->list, list); + map_node->data = data; + map_node->list_ptr = list_node; + HASH_ADD(hh, lruc->map, key, sizeof(struct lruc_composite_key), map_node); + return true; +} + +void* lruc_composite_key_find(struct lruc_composite_key_context* lruc, + struct lruc_composite_key* key) { + struct lruc_composite_key_map_node* mn = get_map_node(lruc, *key); + if (mn) + return mn->data; + return NULL; +} + +void* lruc_composite_key_get(struct lruc_composite_key_context* lruc, + struct lruc_composite_key* key) { + struct lruc_composite_key_map_node* mn = get_map_node(lruc, *key); + if (!mn) + return NULL; + struct lruc_composite_key_list_node* ln = mn->list_ptr; + assert(ln != NULL); + // move node to the front of the list + LISTP_DEL(ln, &lruc->list, list); + LISTP_ADD(ln, &lruc->list, list); + return mn->data; +} + +size_t lruc_composite_key_size(struct lruc_composite_key_context* lruc) { + return HASH_COUNT(lruc->map); +} + +void* lruc_composite_key_get_first(struct lruc_composite_key_context* lruc) { + if (LISTP_EMPTY(&lruc->list)) + return NULL; + + lruc->current = LISTP_FIRST_ENTRY(&lruc->list, /*unused*/ 0, list); + struct lruc_composite_key_map_node* mn = get_map_node(lruc, lruc->current->key); + assert(mn != NULL); + return mn ? mn->data : NULL; +} + +void* lruc_composite_key_get_next(struct lruc_composite_key_context* lruc) { + if (LISTP_EMPTY(&lruc->list) || !lruc->current) + return NULL; + + lruc->current = LISTP_NEXT_ENTRY(lruc->current, &lruc->list, list); + if (!lruc->current) + return NULL; + + struct lruc_composite_key_map_node* mn = get_map_node(lruc, lruc->current->key); + assert(mn != NULL); + return mn ? mn->data : NULL; +} + +void* lruc_composite_key_get_last(struct lruc_composite_key_context* lruc) { + if (LISTP_EMPTY(&lruc->list)) + return NULL; + + struct lruc_composite_key_list_node* ln = LISTP_LAST_ENTRY(&lruc->list, /*unused*/ 0, list); + struct lruc_composite_key_map_node* mn = get_map_node(lruc, ln->key); + assert(mn != NULL); + return mn ? mn->data : NULL; +} + +void lruc_composite_key_remove_last(struct lruc_composite_key_context* lruc) { + if (LISTP_EMPTY(&lruc->list)) + return; + + struct lruc_composite_key_list_node* ln = LISTP_LAST_ENTRY(&lruc->list, /*unused*/ 0, list); + LISTP_DEL(ln, &lruc->list, list); + struct lruc_composite_key_map_node* mn = get_map_node(lruc, ln->key); + assert(mn != NULL); + if (mn) + HASH_DEL(lruc->map, mn); + free(ln); + free(mn); +} diff --git a/libos/src/fs/chroot/trusted.c b/libos/src/fs/chroot/trusted.c index a050ea6f10..b2651f35ba 100644 --- a/libos/src/fs/chroot/trusted.c +++ b/libos/src/fs/chroot/trusted.c @@ -26,17 +26,25 @@ #include "hex.h" #include "libos_fs.h" #include "list.h" +#include "lru_composite_key_cache.h" #include "path_utils.h" #include "toml.h" +#include "toml_utils.h" /* FIXME: current size is 16KB, but maybe there's a better size for perf/mem trade-off? */ #define TRUSTED_CHUNK_SIZE (PAGE_SIZE * 4UL) +struct tf_chunk { + struct lruc_composite_key key; + uint8_t data[TRUSTED_CHUNK_SIZE]; +}; + /* FIXME: use hash table instead of list */ DEFINE_LIST(trusted_file); struct trusted_file { LIST_TYPE(trusted_file) list; struct trusted_file_hash file_hash; /* hash over file, retrieved from the manifest */ + uint64_t usage_count; size_t path_len; char path[]; /* must be NULL-terminated */ }; @@ -45,6 +53,25 @@ struct trusted_file { DEFINE_LISTP(trusted_file); static LISTP_TYPE(trusted_file) g_trusted_file_list = LISTP_INIT; +static spinlock_t g_trusted_file_lock = INIT_SPINLOCK_UNLOCKED; +static struct lruc_composite_key_context* g_lru_composite_key_cache = NULL; +static uint64_t g_tf_chunks_in_cache = 0; + +static const char* strip_prefix(const char* uri) { + const char* s = strchr(uri, ':'); + assert(s); + return s + 1; +} + +static int populate_lru_composite_key_cache(void) { + if (g_tf_chunks_in_cache > 0 && !g_lru_composite_key_cache) { + if (!(g_lru_composite_key_cache = lruc_composite_key_create())) { + return -ENOMEM; + } + } + return 0; +} + static int read_file_exact(PAL_HANDLE handle, void* buffer, uint64_t offset, size_t size) { size_t buffer_offset = 0; size_t remaining = size; @@ -205,8 +232,57 @@ int load_trusted_file(struct trusted_file* tf, size_t file_size, return ret; } -int read_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t count, uint8_t* buf, - size_t file_size, struct trusted_chunk_hash* chunk_hashes) { +static int tf_append_chunk(struct libos_handle* hdl, uint8_t* chunk, + uint64_t chunk_size, struct lruc_composite_key* key) { + if (g_tf_chunks_in_cache == 0) + return 0; + + struct trusted_file* tf = get_trusted_file(strip_prefix(hdl->uri)); + + // Counts the number of times a file is open and reused + if (key->chunk_number == 0 && tf && tf->usage_count <= 10) { + spinlock_lock(&g_trusted_file_lock); + tf->usage_count++; + spinlock_unlock(&g_trusted_file_lock); + } + + // Add file chunks to cache only if the file is reused for 10 times or more + if (tf && tf->usage_count > 10) { + struct tf_chunk* new_chunk = (struct tf_chunk*)malloc(sizeof(struct tf_chunk)); + if (!new_chunk) + return -ENOMEM; + + memcpy(&new_chunk->key, key, sizeof(struct lruc_composite_key)); + memcpy(new_chunk->data, chunk, chunk_size); + + spinlock_lock(&g_trusted_file_lock); + if (!lruc_composite_key_add(g_lru_composite_key_cache, key, new_chunk)) { + free(new_chunk); + spinlock_unlock(&g_trusted_file_lock); + return -ENOMEM; + } + + if (lruc_composite_key_size(g_lru_composite_key_cache) > g_tf_chunks_in_cache) { + free(lruc_composite_key_get_last(g_lru_composite_key_cache)); + lruc_composite_key_remove_last(g_lru_composite_key_cache); +#ifdef DEBUG + static uint64_t tf_cache_log_throttler = 0; + if (++tf_cache_log_throttler == 100) { + log_always("High frequency of this log indicates trusted files cache size exceed" + " the `sgx.trusted_files_cache_size` limit. Please increase it in the" + " manifest file to get the best performance."); + tf_cache_log_throttler = 0; + } +#endif /* DEBUG */ + } + spinlock_unlock(&g_trusted_file_lock); + } + return 0; +} + +int read_and_verify_trusted_file(struct libos_handle* hdl, uint64_t offset, size_t count, + uint8_t* buf, size_t file_size, + struct trusted_chunk_hash* chunk_hashes) { int ret; if (offset >= file_size) @@ -220,6 +296,13 @@ int read_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t coun if (!tmp_chunk) return -ENOMEM; + struct lruc_composite_key* key = + (struct lruc_composite_key*)malloc(sizeof(struct lruc_composite_key)); + if (!key) { + free(tmp_chunk); + return -ENOMEM; + } + uint8_t* buf_pos = buf; uint64_t chunk_offset = aligned_offset; struct trusted_chunk_hash* chunk_hashes_item = chunk_hashes + @@ -228,6 +311,40 @@ int read_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t coun size_t chunk_size = MIN(file_size - chunk_offset, TRUSTED_CHUNK_SIZE); uint64_t chunk_end = chunk_offset + chunk_size; + key->chunk_number = chunk_offset / TRUSTED_CHUNK_SIZE; + key->id = hdl->id; + + struct tf_chunk *chunk = NULL; + + if (g_tf_chunks_in_cache > 0) { + spinlock_lock(&g_trusted_file_lock); + ret = populate_lru_composite_key_cache(); + if (ret < 0) { + spinlock_unlock(&g_trusted_file_lock); + goto out; + } + chunk = (struct tf_chunk*)lruc_composite_key_get(g_lru_composite_key_cache, key); + spinlock_unlock(&g_trusted_file_lock); + } + + if (chunk != NULL) { + if (chunk_offset >= offset && chunk_end <= end) { + memcpy(buf_pos, chunk->data, chunk_size); + + buf_pos += chunk_size; + } else { + off_t copy_start = MAX(chunk_offset, offset); + off_t copy_end = MIN(chunk_offset + (off_t)chunk_size, end); + assert(copy_end > copy_start); + + memcpy(buf_pos, chunk->data + copy_start - chunk_offset, copy_end - copy_start); + buf_pos += copy_end - copy_start; + } + continue; + } + + /* we didn't find the chunk in the trusted file cache, must copy into enclave and add to*/ + /* the trusted file cache */ LIB_SHA256_CONTEXT chunk_sha; ret = lib_SHA256Init(&chunk_sha); if (ret < 0) { @@ -238,7 +355,10 @@ int read_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t coun if (chunk_offset >= offset && chunk_end <= end) { /* if current chunk-to-verify completely resides in the requested region-to-copy, * directly copy into buf (without a scratch buffer) and hash in-place */ - ret = read_file_exact(handle, buf_pos, chunk_offset, chunk_size); + ret = read_file_exact(hdl->pal_handle, buf_pos, chunk_offset, chunk_size); + if (ret < 0) + goto out; + ret = tf_append_chunk(hdl, buf_pos, chunk_size, key); if (ret < 0) goto out; ret = lib_SHA256Update(&chunk_sha, buf_pos, chunk_size); @@ -251,7 +371,10 @@ int read_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t coun /* if current chunk-to-verify only partially overlaps with the requested region-to-copy, * read the file contents into a scratch buffer, verify hash and then copy only the part * needed by the caller */ - ret = read_file_exact(handle, tmp_chunk, chunk_offset, chunk_size); + ret = read_file_exact(hdl->pal_handle, tmp_chunk, chunk_offset, chunk_size); + if (ret < 0) + goto out; + ret = tf_append_chunk(hdl, tmp_chunk, chunk_size, key); if (ret < 0) goto out; ret = lib_SHA256Update(&chunk_sha, tmp_chunk, chunk_size); @@ -288,6 +411,7 @@ int read_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t coun ret = 0; out: free(tmp_chunk); + free(key); return ret; } @@ -426,5 +550,15 @@ int init_trusted_files(void) { return ret; } + uint64_t tf_cache_size = 0; + ret = toml_sizestring_in(g_manifest_root, "sgx.trusted_files_cache_size", 0, + &tf_cache_size); + if (ret < 0) { + log_error("Cannot parse 'sgx.trusted_files_cache_size'"); + return -EINVAL; + } + + g_tf_chunks_in_cache = tf_cache_size / TRUSTED_CHUNK_SIZE; + return 0; } diff --git a/libos/src/meson.build b/libos/src/meson.build index 8a461b2d96..c658bc489c 100644 --- a/libos/src/meson.build +++ b/libos/src/meson.build @@ -20,6 +20,7 @@ libos_sources = files( 'fs/chroot/file_check_policy.c', 'fs/chroot/fs.c', 'fs/chroot/trusted.c', + 'fs/chroot/lru_composite_key_cache.c', 'fs/dev/attestation.c', 'fs/dev/fs.c', 'fs/etc/fs.c', diff --git a/python/graminelibos/manifest_check.py b/python/graminelibos/manifest_check.py index bfba896527..4dc1b57ba9 100644 --- a/python/graminelibos/manifest_check.py +++ b/python/graminelibos/manifest_check.py @@ -108,6 +108,7 @@ 'trusted_files': [Any(str, {'uri': _uri, 'sha256': str})], 'use_exinfo': bool, 'vtune_profile': bool, + 'trusted_files_cache_size': _size, }, 'sys': {