Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LibOS] Add Page Cache feature for trusted files #2011

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CI-Examples/nginx/nginx.manifest.template
Original file line number Diff line number Diff line change
Expand Up @@ -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"
35 changes: 35 additions & 0 deletions Documentation/manifest-syntax.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions Documentation/performance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions libos/include/libos_fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
32 changes: 32 additions & 0 deletions libos/include/lru_composite_key_cache.h
Original file line number Diff line number Diff line change
@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>

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);
2 changes: 1 addition & 1 deletion libos/src/fs/chroot/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
188 changes: 188 additions & 0 deletions libos/src/fs/chroot/lru_composite_key_cache.c
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>

#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);
}
Loading