diff --git a/CI-Examples/nginx/nginx.manifest.template b/CI-Examples/nginx/nginx.manifest.template index 994b770396..94d5db587b 100644 --- a/CI-Examples/nginx/nginx.manifest.template +++ b/CI-Examples/nginx/nginx.manifest.template @@ -33,6 +33,10 @@ sgx.edmm_enable = {{ 'true' if env.get('EDMM', '0') == '1' else 'false' }} sgx.enclave_size = "512M" sgx.max_threads = {{ '1' if env.get('EDMM', '0') == '1' else '4' }} +# One trusted-file chunk of 16k size is enough to cover common Nginx static HTTP file of 10k size +# which will be cached by Gramine +sgx.tf_max_chunks_in_cache = 1 + sgx.trusted_files = [ "file:{{ gramine.libos }}", "file:{{ install_dir }}/sbin/nginx", diff --git a/Documentation/manifest-syntax.rst b/Documentation/manifest-syntax.rst index 5aa0e27a5a..417b391414 100644 --- a/Documentation/manifest-syntax.rst +++ b/Documentation/manifest-syntax.rst @@ -968,6 +968,20 @@ 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-max-chunks-in-cache: + +Trusted files Page Cache optimization: maximum chunks in cache +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + sgx.tf_max_chunks_in_cache = [NUM] + (Default: 0) + +This syntax specifies a limit on the number of chunks in the cache for trusted +files. By default, this optimization is disabled as the limit should be +application-specific. + .. _encrypted-files: Encrypted files diff --git a/Documentation/performance.rst b/Documentation/performance.rst index 515d1d335b..bd47ac6edf 100644 --- a/Documentation/performance.rst +++ b/Documentation/performance.rst @@ -305,6 +305,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.tf_max_chunks_in_cache``. +See :ref:`trusted-files-max-chunks-in-cache` for more information. + .. _choice_of_sgx_machine: Choice of SGX machine diff --git a/common/src/protected_files/lru_cache.h b/common/include/lru_cache.h similarity index 100% rename from common/src/protected_files/lru_cache.h rename to common/include/lru_cache.h diff --git a/common/src/protected_files/lru_cache.c b/common/src/lru_cache.c similarity index 100% rename from common/src/protected_files/lru_cache.c rename to common/src/lru_cache.c diff --git a/common/src/meson.build b/common/src/meson.build index fa7c0851f1..2f622e1429 100644 --- a/common/src/meson.build +++ b/common/src/meson.build @@ -3,6 +3,10 @@ common_src_utils = files( 'string_utils.c', ) +common_src_lru_cache = files( + 'lru_cache.c', +) + common_src_internal = files( 'avl_tree.c', 'init.c', @@ -45,6 +49,16 @@ common_utils_dep = declare_dependency( include_directories: common_inc, ) +common_lru_cache_dep = declare_dependency( + sources: common_src_lru_cache, + + include_directories: common_inc, + + dependencies: [ + uthash_dep, + ], +) + common_dep = declare_dependency( sources: common_src_internal, diff --git a/common/src/protected_files/meson.build b/common/src/protected_files/meson.build index d604050ab9..fbc78737c3 100644 --- a/common/src/protected_files/meson.build +++ b/common/src/protected_files/meson.build @@ -5,10 +5,7 @@ protected_files_inc = [ protected_files_dep = declare_dependency( sources: [ - 'lru_cache.c', 'protected_files.c', - - 'lru_cache.h', 'protected_files_format.h', 'protected_files.h', 'protected_files_internal.h', @@ -17,6 +14,6 @@ protected_files_dep = declare_dependency( include_directories: protected_files_inc, dependencies: [ - uthash_dep, + common_lru_cache_dep, ] ) diff --git a/pal/src/host/linux-sgx/enclave_framework.c b/pal/src/host/linux-sgx/enclave_framework.c index ed9398ed78..e07188db06 100644 --- a/pal/src/host/linux-sgx/enclave_framework.c +++ b/pal/src/host/linux-sgx/enclave_framework.c @@ -23,6 +23,7 @@ static int register_file(const char* uri, const char* hash_str, bool check_dupli uintptr_t g_enclave_base; uintptr_t g_enclave_top; bool g_allowed_files_warn = false; +extern int64_t g_tf_max_chunks_in_cache; /* * SGX's EGETKEY(SEAL_KEY) uses three masks as key-derivation material: @@ -402,7 +403,7 @@ int sgx_get_seal_key(uint16_t key_policy, sgx_key_128bit_t* out_seal_key) { 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; +spinlock_t g_trusted_file_lock = INIT_SPINLOCK_UNLOCKED; static int g_file_check_policy = FILE_CHECK_POLICY_STRICT; static void find_path_in_uri(const char* uri, size_t uri_len, const char** out_path, @@ -516,8 +517,11 @@ int load_trusted_or_allowed_file(struct trusted_file* tf, PAL_HANDLE file, bool goto fail; } } - + assert(!(file->file.cache)); spinlock_lock(&g_trusted_file_lock); + if (g_tf_max_chunks_in_cache > 0 && !(file->file.cache = lruc_create())) + return -PAL_ERROR_NOMEM; + if (tf->chunk_hashes) { *out_chunk_hashes = tf->chunk_hashes; spinlock_unlock(&g_trusted_file_lock); @@ -624,7 +628,53 @@ static void set_file_check_policy(int policy) { g_file_check_policy = policy; } -int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* umem, +static int tf_append_chunk(PAL_HANDLE handle, uint8_t* chunk, + uint64_t chunk_size, uint64_t chunk_number) { + if (g_tf_max_chunks_in_cache == 0) + return 0; + + // Counts the number of times a file is open and reused + if (chunk_number == 0 && handle->file.usage_count <= 10) { + spinlock_lock(&g_trusted_file_lock); + handle->file.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 (handle->file.usage_count > 10) { + struct tf_chunk* new_chunk = (struct tf_chunk*)malloc(sizeof(struct tf_chunk)); + if (!new_chunk) { + return -PAL_ERROR_NOMEM; + } + new_chunk->chunk_number = chunk_number; + memcpy(new_chunk->data, chunk, chunk_size); + + spinlock_lock(&g_trusted_file_lock); + if (!lruc_add(handle->file.cache, chunk_number, new_chunk)) { + free(new_chunk); + return -PAL_ERROR_NOMEM; + } + + if (lruc_size(handle->file.cache) > (size_t) g_tf_max_chunks_in_cache) { + free(lruc_get_last(handle->file.cache)); + lruc_remove_last(handle->file.cache); +#ifdef DEBUG + static int tf_cache_log_throttler = 0; + if (++tf_cache_log_throttler == 100) { + log_always("High frequency of this log indicates trusted files chunks exceed the" + " `sgx.tf_max_chunks_in_cache` 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 copy_and_verify_trusted_file(PAL_HANDLE handle, const char* path, uint8_t* buf, const void* umem, off_t aligned_offset, off_t aligned_end, off_t offset, off_t end, sgx_chunk_hash_t* chunk_hashes, size_t file_size) { int ret = 0; @@ -642,10 +692,38 @@ int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* ume uint8_t* buf_pos = buf; off_t chunk_offset = aligned_offset; - for (; chunk_offset < aligned_end; chunk_offset += TRUSTED_CHUNK_SIZE, chunk_hashes_item++) { + + int chunk_number = chunk_offset/TRUSTED_CHUNK_SIZE; + + for (; chunk_offset < aligned_end; chunk_offset += TRUSTED_CHUNK_SIZE, chunk_hashes_item++, chunk_number++) { size_t chunk_size = MIN(file_size - chunk_offset, TRUSTED_CHUNK_SIZE); off_t chunk_end = chunk_offset + chunk_size; + struct tf_chunk *chunk = NULL; + + if (g_tf_max_chunks_in_cache > 0) { + spinlock_lock(&g_trusted_file_lock); + chunk = (struct tf_chunk*)lruc_get(handle->file.cache, chunk_number); + 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 */ sgx_chunk_hash_t chunk_hash[2]; /* each chunk_hash is 128 bits in size but we need 256 */ LIB_SHA256_CONTEXT chunk_sha; @@ -657,9 +735,14 @@ int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* ume /* if current chunk-to-copy completely resides in the requested region-to-copy, * directly copy into buf (without a scratch buffer) and hash in-place */ if (!sgx_copy_to_enclave(buf_pos, chunk_size, umem + chunk_offset, chunk_size)) { + ret = -PAL_ERROR_DENIED; goto failed; } + ret = tf_append_chunk(handle, buf_pos, chunk_size, chunk_number); + if (ret < 0) + goto failed; + ret = lib_SHA256Update(&chunk_sha, buf_pos, chunk_size); if (ret < 0) goto failed; @@ -670,9 +753,14 @@ int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* ume * read the file contents into a scratch buffer, verify hash and then copy only the part * needed by the caller */ if (!sgx_copy_to_enclave(tmp_chunk, chunk_size, umem + chunk_offset, chunk_size)) { + ret = -PAL_ERROR_DENIED; goto failed; } + ret = tf_append_chunk(handle, tmp_chunk, chunk_size, chunk_number); + if (ret < 0) + goto failed; + ret = lib_SHA256Update(&chunk_sha, tmp_chunk, chunk_size); if (ret < 0) goto failed; @@ -702,6 +790,7 @@ int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* ume return 0; failed: + assert(ret < 0); free(tmp_chunk); memset(buf, 0, end - offset); return ret; diff --git a/pal/src/host/linux-sgx/enclave_tf.h b/pal/src/host/linux-sgx/enclave_tf.h index c930a37aa9..9df73b040b 100644 --- a/pal/src/host/linux-sgx/enclave_tf.h +++ b/pal/src/host/linux-sgx/enclave_tf.h @@ -27,6 +27,7 @@ #include "pal.h" #include "pal_linux_types.h" +extern spinlock_t g_trusted_file_lock; int init_seal_key_material(void); int init_file_check_policy(void); @@ -60,6 +61,7 @@ int load_trusted_or_allowed_file(struct trusted_file* tf, PAL_HANDLE file, bool /*! * \brief Copy and check file contents from untrusted outside buffer to in-enclave buffer * + * \param tf Trusted file struct corresponding to this file. * \param path File path (currently only for a log message). * \param buf In-enclave buffer where contents of the file are copied. * \param umem Start of untrusted file memory mapped outside the enclave. @@ -72,9 +74,10 @@ int load_trusted_or_allowed_file(struct trusted_file* tf, PAL_HANDLE file, bool * * \returns 0 on success, negative error code on failure */ -int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* umem, - off_t aligned_offset, off_t aligned_end, off_t offset, off_t end, - sgx_chunk_hash_t* chunk_hashes, size_t file_size); +int copy_and_verify_trusted_file(PAL_HANDLE handle, const char* path, uint8_t* buf, + const void* umem, off_t aligned_offset, off_t aligned_end, + off_t offset, off_t end,sgx_chunk_hash_t* chunk_hashes, + size_t file_size); int init_trusted_files(void); int init_allowed_files(void); diff --git a/pal/src/host/linux-sgx/meson.build b/pal/src/host/linux-sgx/meson.build index e10defc768..a0d759975c 100644 --- a/pal/src/host/linux-sgx/meson.build +++ b/pal/src/host/linux-sgx/meson.build @@ -125,6 +125,7 @@ libpal_sgx = shared_library('pal', ], dependencies: [ + common_lru_cache_dep, common_dep, cryptoadapter_dep, ioctls_dep, diff --git a/pal/src/host/linux-sgx/pal_files.c b/pal/src/host/linux-sgx/pal_files.c index 0f8c50a868..7ae454270d 100644 --- a/pal/src/host/linux-sgx/pal_files.c +++ b/pal/src/host/linux-sgx/pal_files.c @@ -24,6 +24,8 @@ #include "path_utils.h" #include "stat.h" +extern int64_t g_tf_max_chunks_in_cache; + /* this macro is used to emulate mmap() via pread() in chunks of 128MB (mmapped files may be many * GBs in size, and a pread OCALL could fail with -ENOMEM, so we cap to reasonably small size) */ #define MAX_READ_SIZE (PRESET_PAGESIZE * 1024 * 32) @@ -64,6 +66,8 @@ static int file_open(PAL_HANDLE* handle, const char* type, const char* uri, } init_handle_hdr(hdl, PAL_TYPE_FILE); + hdl->file.cache = NULL; + hdl->file.usage_count = 0; hdl->flags |= PAL_HANDLE_FD_READABLE | PAL_HANDLE_FD_WRITABLE; hdl->file.realpath = normpath; @@ -144,7 +148,7 @@ static int file_open(PAL_HANDLE* handle, const char* type, const char* uri, hdl->file.chunk_hashes = chunk_hashes; hdl->file.total = total; hdl->file.umem = umem; - + hdl->file.tf = tf; *handle = hdl; return 0; @@ -185,9 +189,9 @@ static int64_t file_read(PAL_HANDLE handle, uint64_t offset, uint64_t count, voi off_t aligned_offset = ALIGN_DOWN(offset, TRUSTED_CHUNK_SIZE); off_t aligned_end = ALIGN_UP(end, TRUSTED_CHUNK_SIZE); - ret = copy_and_verify_trusted_file(handle->file.realpath, buffer, handle->file.umem, - aligned_offset, aligned_end, offset, end, chunk_hashes, - total); + ret = copy_and_verify_trusted_file(handle, handle->file.realpath, buffer, + handle->file.umem, aligned_offset, aligned_end, offset, end, + chunk_hashes, total); if (ret < 0) return ret; @@ -225,6 +229,17 @@ static void file_destroy(PAL_HANDLE handle) { ocall_munmap_untrusted(handle->file.umem, handle->file.total); } + if (g_tf_max_chunks_in_cache > 0 && handle->file.cache) { + spinlock_lock(&g_trusted_file_lock); + struct tf_chunk* chunk; + while ((chunk = lruc_get_last(handle->file.cache)) != NULL) { + free(chunk); + lruc_remove_last(handle->file.cache); + } + lruc_destroy(handle->file.cache); + spinlock_unlock(&g_trusted_file_lock); + } + int ret = ocall_close(handle->file.fd); if (ret < 0) { log_error("closing file host fd %d failed: %s", handle->file.fd, unix_strerror(ret)); @@ -309,9 +324,9 @@ static int file_map(PAL_HANDLE handle, void* addr, pal_prot_flags_t prot, uint64 goto out; } - ret = copy_and_verify_trusted_file(handle->file.realpath, addr, handle->file.umem, - aligned_offset, aligned_end, offset, end, chunk_hashes, - handle->file.total); + ret = copy_and_verify_trusted_file(handle, handle->file.realpath, addr, + handle->file.umem, aligned_offset, aligned_end, + offset, end, chunk_hashes, handle->file.total); if (ret < 0) { log_error("file_map - copy & verify on trusted file: %s", pal_strerror(ret)); goto out; diff --git a/pal/src/host/linux-sgx/pal_host.h b/pal/src/host/linux-sgx/pal_host.h index 1248fdef7e..5ebc7fd9be 100644 --- a/pal/src/host/linux-sgx/pal_host.h +++ b/pal/src/host/linux-sgx/pal_host.h @@ -15,6 +15,8 @@ #include #include +#include "lru_cache.h" +#include "pal_linux_defs.h" #include "enclave_tf_structs.h" #include "list.h" #include "spinlock.h" @@ -27,6 +29,11 @@ struct pal_handle_thread { void* param; }; +struct tf_chunk { + uint64_t chunk_number; + uint8_t data[TRUSTED_CHUNK_SIZE]; +}; + /* RPC streams are encrypted with 256-bit AES keys */ typedef uint8_t PAL_SESSION_KEY[32]; @@ -54,6 +61,9 @@ typedef struct { sgx_chunk_hash_t* chunk_hashes; /* array of hashes of file chunks */ void* umem; /* valid only when chunk_hashes != NULL */ bool seekable; /* regular files are seekable, FIFO pipes are not */ + struct trusted_file* tf; + lruc_context_t* cache; + uint64_t usage_count; /* gives a hint on how many times a file was used */ } file; struct { diff --git a/pal/src/host/linux-sgx/pal_main.c b/pal/src/host/linux-sgx/pal_main.c index 3d9cbd867d..0bbd995572 100644 --- a/pal/src/host/linux-sgx/pal_main.c +++ b/pal/src/host/linux-sgx/pal_main.c @@ -409,6 +409,7 @@ extern void* g_enclave_top; extern bool g_allowed_files_warn; extern uint64_t g_tsc_hz; extern size_t g_unused_tcs_pages_num; +int64_t g_tf_max_chunks_in_cache = 0; static int print_warnings_on_insecure_configs(PAL_HANDLE parent_process) { int ret; @@ -864,6 +865,17 @@ noreturn void pal_linux_main(void* uptr_libpal_uri, size_t libpal_uri_len, void* ocall_exit(1, /*is_exitgroup=*/true); } + ret = toml_int_in(g_pal_public_state.manifest_root, "sgx.tf_max_chunks_in_cache", + /*defaultval=*/0, &g_tf_max_chunks_in_cache); + if (ret < 0) { + log_error("Cannot parse 'sgx.tf_max_chunks_in_cache'"); + ocall_exit(1, /*is_exitgroup=*/true); + } + if (g_tf_max_chunks_in_cache < 0) { + log_error("Invalid 'sgx.tf_max_chunks_in_cache' value"); + ocall_exit(1, /*is_exitgroup=*/true); + } + ret = toml_bool_in(g_pal_public_state.manifest_root, "sys.enable_extra_runtime_domain_names_conf", /*defaultval*/false, &g_pal_public_state.extra_runtime_domain_names_conf);