diff --git a/common/src/crypto/adapters/mbedtls_adapter.c b/common/src/crypto/adapters/mbedtls_adapter.c index 41196732b2..aae636da60 100644 --- a/common/src/crypto/adapters/mbedtls_adapter.c +++ b/common/src/crypto/adapters/mbedtls_adapter.c @@ -35,6 +35,7 @@ static int mbedtls_to_pal_error(int error) { case MBEDTLS_ERR_AES_INVALID_INPUT_LENGTH: case MBEDTLS_ERR_CIPHER_FULL_BLOCK_EXPECTED: + case MBEDTLS_ERR_GCM_BUFFER_TOO_SMALL: return PAL_ERROR_CRYPTO_INVALID_INPUT_LENGTH; case MBEDTLS_ERR_CIPHER_FEATURE_UNAVAILABLE: @@ -52,6 +53,8 @@ static int mbedtls_to_pal_error(int error) { case MBEDTLS_ERR_RSA_PUBLIC_FAILED: // see mbedtls_rsa_public() case MBEDTLS_ERR_RSA_PRIVATE_FAILED: // see mbedtls_rsa_private() case MBEDTLS_ERR_ECP_BAD_INPUT_DATA: + case MBEDTLS_ERR_GCM_BAD_INPUT: + case MBEDTLS_ERR_SHA256_BAD_INPUT_DATA: return PAL_ERROR_CRYPTO_BAD_INPUT_DATA; case MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE: @@ -71,6 +74,7 @@ static int mbedtls_to_pal_error(int error) { return PAL_ERROR_CRYPTO_INVALID_PADDING; case MBEDTLS_ERR_CIPHER_AUTH_FAILED: + case MBEDTLS_ERR_GCM_AUTH_FAILED: return PAL_ERROR_CRYPTO_AUTH_FAILED; case MBEDTLS_ERR_CIPHER_INVALID_CONTEXT: diff --git a/common/src/protected_files/protected_files.c b/common/src/protected_files/protected_files.c index 2fbac44c1f..fc1e2b82a1 100644 --- a/common/src/protected_files/protected_files.c +++ b/common/src/protected_files/protected_files.c @@ -643,8 +643,10 @@ static file_node_t* ipf_read_data_node(pf_context_t* pf, uint64_t offset) { if (PF_FAILURE(status)) { free(file_data_node); pf->last_error = status; - if (status == PF_STATUS_MAC_MISMATCH) + if (status == PF_STATUS_MAC_MISMATCH) { pf->file_status = PF_STATUS_CORRUPTED; + pf->last_error = PF_STATUS_CORRUPTED; + } return NULL; } @@ -706,8 +708,10 @@ static file_node_t* ipf_read_mht_node(pf_context_t* pf, uint64_t logical_mht_nod if (PF_FAILURE(status)) { free(file_mht_node); pf->last_error = status; - if (status == PF_STATUS_MAC_MISMATCH) + if (status == PF_STATUS_MAC_MISMATCH) { pf->file_status = PF_STATUS_CORRUPTED; + pf->last_error = PF_STATUS_CORRUPTED; + } return NULL; } @@ -791,6 +795,11 @@ static bool ipf_init_existing_file(pf_context_t* pf, const char* path) { if (PF_FAILURE(status)) { pf->last_error = status; DEBUG_PF("failed to decrypt metadata: %d", status); + if (status == PF_STATUS_MAC_MISMATCH) { + // MAC could also mismatch if wrong key was provided but we err on side of safety ... + pf->file_status = PF_STATUS_CORRUPTED; + pf->last_error = PF_STATUS_CORRUPTED; + } return false; } @@ -818,6 +827,10 @@ static bool ipf_init_existing_file(pf_context_t* pf, const char* path) { &pf->metadata_decrypted.root_mht_node_mac); if (PF_FAILURE(status)) { pf->last_error = status; + if (status == PF_STATUS_MAC_MISMATCH) { + pf->file_status = PF_STATUS_CORRUPTED; + pf->last_error = PF_STATUS_CORRUPTED; + } return false; } } @@ -1076,7 +1089,7 @@ static void ipf_delete_cache(pf_context_t* pf) { } } -static bool ipf_close(pf_context_t* pf) { +static bool ipf_close(pf_context_t* pf, pf_mac_t* closing_root_mac) { bool retval = true; if (pf->file_status != PF_STATUS_SUCCESS) { @@ -1089,6 +1102,10 @@ static bool ipf_close(pf_context_t* pf) { } } + if (closing_root_mac != NULL) { + memcpy(*closing_root_mac, pf->metadata_node.plaintext_part.metadata_mac, sizeof(pf_mac_t)); + } + // omeg: fs close is done by Gramine handler pf->file_status = PF_STATUS_UNINITIALIZED; @@ -1126,20 +1143,25 @@ void pf_set_callbacks(pf_read_f read_f, pf_write_f write_f, pf_fsync_f fsync_f, } pf_status_t pf_open(pf_handle_t handle, const char* path, uint64_t underlying_size, - pf_file_mode_t mode, bool create, const pf_key_t* key, pf_context_t** context) { + pf_file_mode_t mode, bool create, const pf_key_t* key, + pf_mac_t* opening_root_mac, pf_context_t** context) { if (!g_initialized) return PF_STATUS_UNINITIALIZED; pf_status_t status; *context = ipf_open(path, mode, create, handle, underlying_size, key, &status); + if ((*context != NULL) && (opening_root_mac != NULL)) { + memcpy(*opening_root_mac, (*context)->metadata_node.plaintext_part.metadata_mac, + sizeof(pf_mac_t)); + } return status; } -pf_status_t pf_close(pf_context_t* pf) { +pf_status_t pf_close(pf_context_t* pf, pf_mac_t* closing_root_mac) { if (!g_initialized) return PF_STATUS_UNINITIALIZED; - if (ipf_close(pf)) { + if (ipf_close(pf, closing_root_mac)) { free(pf); return PF_STATUS_SUCCESS; } @@ -1153,6 +1175,9 @@ pf_status_t pf_get_size(pf_context_t* pf, uint64_t* size) { if (!g_initialized) return PF_STATUS_UNINITIALIZED; + if (pf->file_status == PF_STATUS_CORRUPTED) + return pf->file_status; // Make corruption "sticky" + *size = pf->metadata_decrypted.file_size; return PF_STATUS_SUCCESS; } @@ -1164,6 +1189,9 @@ pf_status_t pf_set_size(pf_context_t* pf, uint64_t size) { if (!(pf->mode & PF_FILE_MODE_WRITE)) return PF_STATUS_INVALID_MODE; + if (pf->file_status == PF_STATUS_CORRUPTED) + return pf->file_status; // Make corruption "sticky" + if (size == pf->metadata_decrypted.file_size) return PF_STATUS_SUCCESS; @@ -1208,10 +1236,13 @@ pf_status_t pf_set_size(pf_context_t* pf, uint64_t size) { return PF_STATUS_SUCCESS; } -pf_status_t pf_rename(pf_context_t* pf, const char* new_path) { +pf_status_t pf_rename(pf_context_t* pf, const char* new_path, pf_mac_t* new_root_mac) { if (!g_initialized) return PF_STATUS_UNINITIALIZED; + if (pf->file_status == PF_STATUS_CORRUPTED) + return pf->file_status; // Make corruption "sticky" + if (!(pf->mode & PF_FILE_MODE_WRITE)) return PF_STATUS_INVALID_MODE; @@ -1224,6 +1255,9 @@ pf_status_t pf_rename(pf_context_t* pf, const char* new_path) { pf->need_writing = true; if (!ipf_internal_flush(pf)) return pf->last_error; + if (new_root_mac != NULL) { + memcpy(*new_root_mac, pf->metadata_node.plaintext_part.metadata_mac, sizeof(pf_mac_t)); + } return PF_STATUS_SUCCESS; } @@ -1281,6 +1315,9 @@ pf_status_t pf_flush(pf_context_t* pf) { if (!g_initialized) return PF_STATUS_UNINITIALIZED; + if (pf->file_status == PF_STATUS_CORRUPTED) + return pf->file_status; // Make corruption "sticky" + if (!ipf_internal_flush(pf)) return pf->last_error; @@ -1297,3 +1334,7 @@ pf_status_t pf_flush(pf_context_t* pf) { return PF_STATUS_SUCCESS; } + +void pf_set_corrupted(pf_context_t* pf) { + pf->file_status = PF_STATUS_CORRUPTED; +} diff --git a/common/src/protected_files/protected_files.h b/common/src/protected_files/protected_files.h index 4bebe71a5c..27d6b0bdc5 100644 --- a/common/src/protected_files/protected_files.h +++ b/common/src/protected_files/protected_files.h @@ -31,6 +31,11 @@ typedef uint8_t pf_mac_t[PF_MAC_SIZE]; typedef uint8_t pf_key_t[PF_KEY_SIZE]; typedef uint8_t pf_nonce_t[PF_NONCE_SIZE]; +// convenience macros to print out some mac fingerprint: printf( "some text " MAC_PRINTF_PATTERN " +// yet other text", MAC_PRINTF_ARGS(mac) ); +#define MAC_PRINTF_PATTERN "0x%02x%02x%02x%02x..." +#define MAC_PRINTF_ARGS(mac) (mac)[0], (mac)[1], (mac)[2], (mac)[3] + typedef enum _pf_status_t { PF_STATUS_SUCCESS = 0, PF_STATUS_UNKNOWN_ERROR = -1, @@ -212,28 +217,31 @@ const char* pf_strerror(int err); /*! * \brief Open a protected file. * - * \param handle Open underlying file handle. - * \param path Path to the file. If NULL and \p create is false, don't check path - * for validity. - * \param underlying_size Underlying file size. - * \param mode Access mode. - * \param create Overwrite file contents if true. - * \param key Wrap key. - * \param[out] context PF context for later calls. + * \param handle Open underlying file handle. + * \param path Path to the file. If NULL and \p create is false, don't check path + * for validity. + * \param underlying_size Underlying file size. + * \param mode Access mode. + * \param create Overwrite file contents if true. + * \param key Wrap key. + * \param opening_root_mac If non-NULL, !create & successfull open, returns root-hash of file + * \param[out] context PF context for later calls. * * \returns PF status. */ pf_status_t pf_open(pf_handle_t handle, const char* path, uint64_t underlying_size, - pf_file_mode_t mode, bool create, const pf_key_t* key, pf_context_t** context); + pf_file_mode_t mode, bool create, const pf_key_t* key, + pf_mac_t* opening_root_mac, pf_context_t** context); /*! * \brief Close a protected file and commit all changes to disk. * - * \param pf PF context. + * \param pf PF context. + * \param closing_root_mac If non-NULL, returns root-hash of file at closing time * * \returns PF status. */ -pf_status_t pf_close(pf_context_t* pf); +pf_status_t pf_close(pf_context_t* pf, pf_mac_t* closing_root_mac); /*! * \brief Read from a protected file. @@ -286,13 +294,14 @@ pf_status_t pf_set_size(pf_context_t* pf, uint64_t size); /*! * \brief Rename a PF. * - * \param pf PF context. - * \param new_path New file path. + * \param pf PF context. + * \param new_path New file path. + * \param new_root_mac if non-NULL, returns new root-hash of file * * Updates the path inside protected file header, and flushes all changes. The caller is responsible * for renaming the underlying file. */ -pf_status_t pf_rename(pf_context_t* pf, const char* new_path); +pf_status_t pf_rename(pf_context_t* pf, const char* new_path, pf_mac_t* new_root_mac); /*! * \brief Flush any pending data of a protected file to disk. @@ -302,3 +311,10 @@ pf_status_t pf_rename(pf_context_t* pf, const char* new_path); * \returns PF status. */ pf_status_t pf_flush(pf_context_t* pf); + +/*! + * \brief Set protected file state as corrupted + * + * \param pf PF context. + */ +void pf_set_corrupted(pf_context_t* pf); diff --git a/libos/include/libos_fs.h b/libos/include/libos_fs.h index 0590a9c8db..5853366a76 100644 --- a/libos/include/libos_fs.h +++ b/libos/include/libos_fs.h @@ -35,6 +35,9 @@ struct libos_mount_params { /* Key name (used by `chroot_encrypted` filesystem), or NULL if not applicable */ const char* key_name; + + /* Enforcement type (used by `chroot_encrypted` filesystem), or NULL if not applicable */ + const char* protection_mode; }; struct libos_fs_ops { diff --git a/libos/include/libos_fs_encrypted.h b/libos/include/libos_fs_encrypted.h index 9890058c51..c5aaebc612 100644 --- a/libos/include/libos_fs_encrypted.h +++ b/libos/include/libos_fs_encrypted.h @@ -14,6 +14,7 @@ #include +#include "libos_checkpoint.h" // for include of uthash.h _and_ consistent uthash_fatal macros #include "libos_types.h" #include "list.h" #include "pal.h" @@ -34,6 +35,48 @@ struct libos_encrypted_files_key { LIST_TYPE(libos_encrypted_files_key) list; }; +typedef enum { + PF_FILE_STATE_ERROR = 0, // file is in non-determined state due to some errors + PF_FILE_STATE_ACTIVE = 1, // file was provisously seen with known (good committed) state + PF_FILE_STATE_DELETED = 2, // file was previously seen but then either unlinked or renamed +} libos_encrypted_file_state_t; + +inline static const char* file_state_to_string(libos_encrypted_file_state_t state) { + return (state == PF_FILE_STATE_ERROR ? "error" + : (state == PF_FILE_STATE_ACTIVE ? "active" : "deleted")); +} + +/* + * Map mapping file URIs to state providing information on files, in particular whether we have seen + * them before and what the last seen root-hash is. This is necessary to provide rollback + */ +struct libos_encrypted_volume_state_map { + char* norm_path; // assumptions: all paths canonicalized, symlinks are resolved & no hard links + libos_encrypted_file_state_t state; + pf_mac_t last_seen_root_mac; // valid only if state == PF_FILE_STATE_ACTIVE + UT_hash_handle hh; +}; + +typedef enum { + PF_ENCLAVE_LIFE_RB_PROTECTION_NONE = 0, + PF_ENCLAVE_LIFE_RB_PROTECTION_NON_STRICT = 1, + PF_ENCLAVE_LIFE_RB_PROTECTION_STRICT = 2, +} libos_encrypted_files_mode_t; + +DEFINE_LIST(libos_encrypted_volume); +DEFINE_LISTP(libos_encrypted_volume); +struct libos_encrypted_volume { + char* mount_point_path; + libos_encrypted_files_mode_t protection_mode; + + struct libos_encrypted_volume_state_map* files_state_map; + struct libos_lock files_state_map_lock; + + struct libos_encrypted_files_key* key; + + LIST_TYPE(libos_encrypted_volume) list; +}; + /* * Represents a specific encrypted file. The file is open as long as `use_count` is greater than 0. * Note that the file can be open and closed multiple times before it's destroyed. @@ -44,7 +87,7 @@ struct libos_encrypted_files_key { struct libos_encrypted_file { size_t use_count; char* uri; - struct libos_encrypted_files_key* key; + struct libos_encrypted_volume* volume; /* `pf` and `pal_handle` are non-null as long as `use_count` is greater than 0 */ pf_context_t* pf; @@ -106,18 +149,45 @@ bool read_encrypted_files_key(struct libos_encrypted_files_key* key, pf_key_t* p */ void update_encrypted_files_key(struct libos_encrypted_files_key* key, const pf_key_t* pf_key); +/* + * \brief Register a volume. + * + * Registers passed volume -- assumed to be initialized, in particular with valid mount_point_path + * -- in global list of mounted volumes. Returns an error if a volume with identical + * mount_point_path already exists. + */ +int register_encrypted_volume(struct libos_encrypted_volume* volume); + +/* + * \brief Retrieve a volume. + * + * Returns a volume with a given mount_point_path, or NULL if it has not been created yet. Note that + * even if the key exists, it might not be set yet (see `struct libos_encrypted_files_key`). + * + * This does not pass ownership of the key: the key objects are still managed by this module. + */ +struct libos_encrypted_volume* get_encrypted_volume(const char* mount_point_path); + +/* + * \brief List existing volumes. + * + * Calls `callback` on each currently existing volume. + */ +int list_encrypted_volumes(int (*callback)(struct libos_encrypted_volume* volume, void* arg), + void* arg); + /* * \brief Open an existing encrypted file. * * \param uri PAL URI to open, has to begin with "file:". - * \param key Key, has to be already set. + * \param volume Volume assocated with file, has to be already set. * \param[out] out_enc On success, set to a newly created `libos_encrypted_file` object. * * `uri` has to correspond to an existing file that can be decrypted with `key`. * * The newly created `libos_encrypted_file` object will have `use_count` set to 1. */ -int encrypted_file_open(const char* uri, struct libos_encrypted_files_key* key, +int encrypted_file_open(const char* uri, struct libos_encrypted_volume* volume, struct libos_encrypted_file** out_enc); /* @@ -125,14 +195,14 @@ int encrypted_file_open(const char* uri, struct libos_encrypted_files_key* key, * * \param uri PAL URI to open, has to begin with "file:". * \param perm Permissions for the new file. - * \param key Key, has to be already set. + * \param volume Volume assocated with file, has to be already set. * \param[out] out_enc On success, set to a newly created `libos_encrypted_file` object. * * `uri` must not correspond to an existing file. * * The newly created `libos_encrypted_file` object will have `use_count` set to 1. */ -int encrypted_file_create(const char* uri, mode_t perm, struct libos_encrypted_files_key* key, +int encrypted_file_create(const char* uri, mode_t perm, struct libos_encrypted_volume* volume, struct libos_encrypted_file** out_enc); /* @@ -154,20 +224,22 @@ int encrypted_file_get(struct libos_encrypted_file* enc); * * This decreases `use_count`, and closes the file if it reaches 0. */ -void encrypted_file_put(struct libos_encrypted_file* enc); +void encrypted_file_put(struct libos_encrypted_file* enc, bool fs_reachable); /* * \brief Flush pending writes to an encrypted file. */ -int encrypted_file_flush(struct libos_encrypted_file* enc); +int encrypted_file_flush(struct libos_encrypted_file* enc, bool fs_reachable); int encrypted_file_read(struct libos_encrypted_file* enc, void* buf, size_t buf_size, - file_off_t offset, size_t* out_count); + file_off_t offset, size_t* out_count, bool fs_reachable); int encrypted_file_write(struct libos_encrypted_file* enc, const void* buf, size_t buf_size, - file_off_t offset, size_t* out_count); + file_off_t offset, size_t* out_count, bool fs_reachable); int encrypted_file_rename(struct libos_encrypted_file* enc, const char* new_uri); +int encrypted_file_unlink(struct libos_encrypted_file* enc); -int encrypted_file_get_size(struct libos_encrypted_file* enc, file_off_t* out_size); -int encrypted_file_set_size(struct libos_encrypted_file* enc, file_off_t size); +int encrypted_file_get_size(struct libos_encrypted_file* enc, file_off_t* out_size, + bool fs_reachable); +int encrypted_file_set_size(struct libos_encrypted_file* enc, file_off_t size, bool fs_reachable); int parse_pf_key(const char* key_str, pf_key_t* pf_key); diff --git a/libos/include/libos_fs_pseudo.h b/libos/include/libos_fs_pseudo.h index d0b37cddf2..d64bd61f33 100644 --- a/libos/include/libos_fs_pseudo.h +++ b/libos/include/libos_fs_pseudo.h @@ -233,6 +233,7 @@ int proc_ipc_thread_follow_link(struct libos_dentry* dent, char** out_target); int init_devfs(void); int init_attestation(struct pseudo_node* dev); +int init_rollback(struct pseudo_node* dev); /* sysfs */ diff --git a/libos/include/libos_handle.h b/libos/include/libos_handle.h index d0920cff06..cdabc3cf41 100644 --- a/libos/include/libos_handle.h +++ b/libos/include/libos_handle.h @@ -165,6 +165,7 @@ struct libos_handle { refcount_t ref_count; struct libos_fs* fs; + /* dentry can change due to rename, so to derefence or update requires holding `lock`. */ struct libos_dentry* dentry; /* diff --git a/libos/include/libos_process.h b/libos/include/libos_process.h index 6478d00370..e2637d484d 100644 --- a/libos/include/libos_process.h +++ b/libos/include/libos_process.h @@ -62,7 +62,7 @@ struct libos_process { LISTP_TYPE(libos_child_process) zombies; struct libos_lock children_lock; - struct libos_lock fs_lock; + struct libos_lock fs_lock; /* Note: has lower priority than g_dcache_lock. */ /* Complete command line for the process, as reported by /proc/[pid]/cmdline; currently filled * once during initialization and not able to be modified. diff --git a/libos/src/bookkeep/libos_handle.c b/libos/src/bookkeep/libos_handle.c index 246eedb076..de2426fa62 100644 --- a/libos/src/bookkeep/libos_handle.c +++ b/libos/src/bookkeep/libos_handle.c @@ -301,25 +301,28 @@ static struct libos_handle* __detach_fd_handle(struct libos_fd_handle* fd, int* } static int clear_posix_locks(struct libos_handle* handle) { - if (handle && handle->dentry) { - /* Clear file (POSIX) locks for a file. We are required to do that every time a FD is - * closed, even if the process holds other handles for that file, or duplicated FDs for the - * same handle. */ - struct libos_file_lock file_lock = { - .family = FILE_LOCK_POSIX, - .type = F_UNLCK, - .start = 0, - .end = FS_LOCK_EOF, - .pid = g_process.pid, - }; - int ret = file_lock_set(handle->dentry, &file_lock, /*block=*/false); - if (ret < 0) { - log_warning("error releasing locks: %s", unix_strerror(ret)); - return ret; + int ret = 0; + if (handle) { + lock(&handle->lock); + if (handle->dentry) { + /* Clear file (POSIX) locks for a file. We are required to do that every time a FD is + * closed, even if the process holds other handles for that file, or duplicated FDs for + * the same handle. */ + struct libos_file_lock file_lock = { + .family = FILE_LOCK_POSIX, + .type = F_UNLCK, + .start = 0, + .end = FS_LOCK_EOF, + .pid = g_process.pid, + }; + ret = file_lock_set(handle->dentry, &file_lock, /*block=*/false); + if (ret < 0) { + log_warning("error releasing locks: %s", unix_strerror(ret)); + } } + unlock(&handle->lock); } - - return 0; + return ret; } struct libos_handle* detach_fd_handle(uint32_t fd, int* flags, @@ -476,16 +479,6 @@ int set_new_fd_handle_above_fd(uint32_t fd, struct libos_handle* hdl, int fd_fla return __set_new_fd_handle(fd, hdl, fd_flags, handle_map, /*find_first=*/true); } -static inline __attribute__((unused)) const char* __handle_name(struct libos_handle* hdl) { - if (hdl->uri) - return hdl->uri; - if (hdl->dentry && hdl->dentry->name[0] != '\0') - return hdl->dentry->name; - if (hdl->fs) - return hdl->fs->name; - return "(unknown)"; -} - void get_handle(struct libos_handle* hdl) { refcount_inc(&hdl->ref_count); } @@ -499,20 +492,24 @@ static void destroy_handle(struct libos_handle* hdl) { static int clear_flock_locks(struct libos_handle* hdl) { /* Clear flock (BSD) locks for a file. We are required to do that when the handle is closed. */ - if (hdl && hdl->dentry && hdl->created_by_process) { - assert(hdl->ref_count == 0); - struct libos_file_lock file_lock = { - .family = FILE_LOCK_FLOCK, - .type = F_UNLCK, - .handle_id = hdl->id, - }; - int ret = file_lock_set(hdl->dentry, &file_lock, /*block=*/false); - if (ret < 0) { - log_warning("error releasing locks: %s", unix_strerror(ret)); - return ret; + int ret = 0; + if (hdl) { + lock(&hdl->lock); + if (hdl->dentry && hdl->created_by_process) { + assert(hdl->ref_count == 0); + struct libos_file_lock file_lock = { + .family = FILE_LOCK_FLOCK, + .type = F_UNLCK, + .handle_id = hdl->id, + }; + int ret = file_lock_set(hdl->dentry, &file_lock, /*block=*/false); + if (ret < 0) { + log_warning("error releasing locks: %s", unix_strerror(ret)); + } } + unlock(&hdl->lock); } - return 0; + return ret; } void put_handle(struct libos_handle* hdl) { @@ -536,7 +533,7 @@ void put_handle(struct libos_handle* hdl) { hdl->pal_handle = NULL; } - if (hdl->dentry) { + if (hdl->dentry) { /* no locking needed as no other reference exists */ (void)clear_flock_locks(hdl); put_dentry(hdl->dentry); } diff --git a/libos/src/fs/chroot/encrypted.c b/libos/src/fs/chroot/encrypted.c index 27c46551dd..df98c5d024 100644 --- a/libos/src/fs/chroot/encrypted.c +++ b/libos/src/fs/chroot/encrypted.c @@ -68,27 +68,70 @@ static int chroot_encrypted_mount(struct libos_mount_params* params, void** moun if (ret < 0) return ret; - *mount_data = key; + libos_encrypted_files_mode_t protection_mode = PF_ENCLAVE_LIFE_RB_PROTECTION_NON_STRICT; + /* default mode non-strict: balances security with backwards-compatibility */ + if (params->protection_mode) { + if (strncmp(params->protection_mode, "strict", strlen("strict")) == 0) + protection_mode = PF_ENCLAVE_LIFE_RB_PROTECTION_STRICT; + else if (strncmp(params->protection_mode, "non-strict", strlen("non-strict")) == 0) + protection_mode = PF_ENCLAVE_LIFE_RB_PROTECTION_NON_STRICT; + else if (strncmp(params->protection_mode, "none", strlen("none")) == 0) + protection_mode = PF_ENCLAVE_LIFE_RB_PROTECTION_NONE; + else { + log_error("Invalid enforcement type: %s", params->protection_mode); + return -EINVAL; + } + } + + struct libos_encrypted_volume* volume; + volume = calloc(1, sizeof(*volume)); + if (!volume) + return -ENOMEM; + volume->mount_point_path = strdup(params->path); + if (!volume->mount_point_path) { + ret = -ENOMEM; + goto err; + } + volume->protection_mode = protection_mode; + volume->key = key; + if (!create_lock(&volume->files_state_map_lock)) { + ret = -ENOMEM; + goto err; + } + volume->files_state_map = NULL; + + ret = register_encrypted_volume(volume); + if (ret < 0) + goto err; + + *mount_data = volume; return 0; +err: + if (volume) { + if (lock_created(&volume->files_state_map_lock)) + destroy_lock(&volume->files_state_map_lock); + free(volume->mount_point_path); + } + free(volume); + return ret; } static ssize_t chroot_encrypted_checkpoint(void** checkpoint, void* mount_data) { - struct libos_encrypted_files_key* key = mount_data; + struct libos_encrypted_volume* volume = mount_data; - *checkpoint = strdup(key->name); + *checkpoint = strdup(volume->mount_point_path); if (!*checkpoint) return -ENOMEM; - return strlen(key->name) + 1; + return strlen(volume->mount_point_path) + 1; } static int chroot_encrypted_migrate(void* checkpoint, void** mount_data) { - const char* name = checkpoint; + const char* mount_point_path = checkpoint; - struct libos_encrypted_files_key* key; - int ret = get_or_create_encrypted_files_key(name, &key); - if (ret < 0) - return ret; - *mount_data = key; + struct libos_encrypted_volume* volume = get_encrypted_volume(mount_point_path); + if (!volume) + return -EEXIST; + *mount_data = volume; return 0; } @@ -153,8 +196,8 @@ static int chroot_encrypted_lookup(struct libos_dentry* dent) { struct libos_encrypted_file* enc; file_off_t size; - struct libos_encrypted_files_key* key = dent->mount->data; - ret = encrypted_file_open(uri, key, &enc); + struct libos_encrypted_volume* volume = dent->mount->data; + ret = encrypted_file_open(uri, volume, &enc); if (ret < 0) { if (ret == -EACCES) { /* allow the inode to be created even if the underlying encrypted file is corrupted; @@ -164,8 +207,8 @@ static int chroot_encrypted_lookup(struct libos_dentry* dent) { goto out; } } else { - ret = encrypted_file_get_size(enc, &size); - encrypted_file_put(enc); + ret = encrypted_file_get_size(enc, &size, true); + encrypted_file_put(enc, true); if (ret < 0) { encrypted_file_destroy(enc); @@ -210,7 +253,7 @@ static int chroot_encrypted_open(struct libos_handle* hdl, struct libos_dentry* get_inode(dent->inode); hdl->type = TYPE_CHROOT_ENCRYPTED; hdl->seekable = true; - hdl->pos = 0; + hdl->pos = 0; return 0; } @@ -231,9 +274,9 @@ static int chroot_encrypted_creat(struct libos_handle* hdl, struct libos_dentry* goto out; } - struct libos_encrypted_files_key* key = dent->mount->data; + struct libos_encrypted_volume* volume = dent->mount->data; struct libos_encrypted_file* enc; - ret = encrypted_file_create(uri, HOST_PERM(perm), key, &enc); + ret = encrypted_file_create(uri, HOST_PERM(perm), volume, &enc); if (ret < 0) goto out; @@ -301,6 +344,13 @@ static int chroot_encrypted_unlink(struct libos_dentry* dent) { if (ret < 0) return ret; + struct libos_encrypted_file* enc = dent->inode->data; + if (!enc) + return -EACCES; + ret = encrypted_file_unlink(enc); + if (ret < 0) + return ret; + PAL_HANDLE palhdl; ret = PalStreamOpen(uri, PAL_ACCESS_RDONLY, /*share_flags=*/0, PAL_CREATE_NEVER, PAL_OPTION_PASSTHROUGH, &palhdl); @@ -346,7 +396,7 @@ static int chroot_encrypted_rename(struct libos_dentry* old, struct libos_dentry goto out; ret = encrypted_file_rename(enc, new_uri); - encrypted_file_put(enc); + encrypted_file_put(enc, true); out: unlock(&old->inode->lock); free(new_uri); @@ -418,7 +468,7 @@ static int chroot_encrypted_flush(struct libos_handle* hdl) { /* This will write changes from `enc` to host file */ lock(&hdl->inode->lock); - ret = encrypted_file_flush(enc); + ret = encrypted_file_flush(enc, hdl->inode == hdl->dentry->inode); unlock(&hdl->inode->lock); return ret; } @@ -432,7 +482,7 @@ static int chroot_encrypted_close(struct libos_handle* hdl) { assert(enc); lock(&hdl->inode->lock); - encrypted_file_put(enc); + encrypted_file_put(enc, hdl->inode == hdl->dentry->inode); unlock(&hdl->inode->lock); return 0; @@ -452,7 +502,8 @@ static ssize_t chroot_encrypted_read(struct libos_handle* hdl, void* buf, size_t size_t actual_count; lock(&hdl->inode->lock); - int ret = encrypted_file_read(enc, buf, count, *pos, &actual_count); + int ret = + encrypted_file_read(enc, buf, count, *pos, &actual_count, hdl->inode == hdl->dentry->inode); unlock(&hdl->inode->lock); if (ret < 0) @@ -477,7 +528,8 @@ static ssize_t chroot_encrypted_write(struct libos_handle* hdl, const void* buf, lock(&hdl->inode->lock); - int ret = encrypted_file_write(enc, buf, count, *pos, &actual_count); + int ret = encrypted_file_write(enc, buf, count, *pos, &actual_count, + hdl->inode == hdl->dentry->inode); if (ret < 0) { unlock(&hdl->inode->lock); return ret; @@ -507,7 +559,7 @@ static int chroot_encrypted_truncate(struct libos_handle* hdl, file_off_t size) assert(enc); lock(&hdl->inode->lock); - ret = encrypted_file_set_size(enc, size); + ret = encrypted_file_set_size(enc, size, hdl->inode == hdl->dentry->inode); if (ret < 0) { unlock(&hdl->inode->lock); return ret; diff --git a/libos/src/fs/dev/fs.c b/libos/src/fs/dev/fs.c index 3068ea40c2..e013178209 100644 --- a/libos/src/fs/dev/fs.c +++ b/libos/src/fs/dev/fs.c @@ -176,5 +176,9 @@ int init_devfs(void) { if (ret < 0) return ret; + ret = init_rollback(root); + if (ret < 0) + return ret; + return 0; } diff --git a/libos/src/fs/dev/rollback.c b/libos/src/fs/dev/rollback.c new file mode 100644 index 0000000000..f0b4fda657 --- /dev/null +++ b/libos/src/fs/dev/rollback.c @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* Copyright (C) 2024 Intel Labs + * Michael Steiner + */ + +/*! + * \file + * + * This file contains a pseudo-device for an application to inspect the rollback protection state. + * `/dev/rollback/ pseudo-file. + * + */ + +// TODO (MST): also add pseudo file to get hash of the last seen root hash (or, better for +// atomicity, status ahd hash) + +#include "api.h" +#include "libos_fs_encrypted.h" +#include "libos_fs_pseudo.h" +#include "pal.h" +#include "toml_utils.h" + +static int path_load(struct libos_dentry* dent, char** out_data, size_t* out_size) { + // TODO (MST): implement me + // - find volume matching path + // - libos/include/libos_fs.h:int walk_mounts(int (*walk)(struct libos_mount* mount, void* + // arg), void* arg); + // - libos_mount* find_mount_from_uri(const char* uri) path_lookupat(start, path, + // - lookup_flags, &dent); + // + // - find (relative) path in map + /* + struct libos_encrypted_volume_state_map* file_state = NULL; + lock(&(enc->volume->files_state_map_lock)); + HASH_FIND_STR(enc->volume->files_state_map, norm_path, file_state); + unlock(&(enc->volume->files_state_map_lock)); + */ + // - prepare outpub buffer with map entry + /* + if (is_set) { + char* buf = malloc(sizeof(pf_key)); + if (!buf) + return -ENOMEM; + memcpy(buf, &pf_key, sizeof(pf_key)); + + *out_data = buf; + *out_size = sizeof(pf_key); + } else { + *out_data = NULL; + *out_size = 0; + } + */ + __UNUSED(dent); + __UNUSED(out_data); + __UNUSED(out_size); + return 0; +} + +int init_rollback(struct pseudo_node* dev) { + struct pseudo_node* rollback_dir = pseudo_add_dir(dev, "rollback"); + pseudo_add_str(rollback_dir, "file_status", &path_load); + + return 0; +} \ No newline at end of file diff --git a/libos/src/fs/libos_fs.c b/libos/src/fs/libos_fs.c index 5a29a36d6d..8733cd444d 100644 --- a/libos/src/fs/libos_fs.c +++ b/libos/src/fs/libos_fs.c @@ -195,10 +195,11 @@ static int mount_one_nonroot(toml_table_t* mount, const char* prefix) { int ret; - char* mount_type = NULL; - char* mount_path = NULL; - char* mount_uri = NULL; - char* mount_key_name = NULL; + char* mount_type = NULL; + char* mount_path = NULL; + char* mount_uri = NULL; + char* mount_key_name = NULL; + char* mount_protection_mode = NULL; ret = toml_string_in(mount, "type", &mount_type); if (ret < 0) { @@ -228,6 +229,13 @@ static int mount_one_nonroot(toml_table_t* mount, const char* prefix) { goto out; } + ret = toml_string_in(mount, "protection_mode", &mount_protection_mode); + if (ret < 0) { + log_error("Cannot parse '%s.key_name'", prefix); + ret = -EINVAL; + goto out; + } + if (!mount_path) { log_error("No value provided for '%s.path'", prefix); ret = -EINVAL; @@ -269,10 +277,11 @@ static int mount_one_nonroot(toml_table_t* mount, const char* prefix) { } struct libos_mount_params params = { - .type = mount_type ?: "chroot", - .path = mount_path, - .uri = mount_uri, - .key_name = mount_key_name, + .type = mount_type ?: "chroot", + .path = mount_path, + .uri = mount_uri, + .key_name = mount_key_name, + .protection_mode = mount_protection_mode, }; ret = mount_fs(¶ms); diff --git a/libos/src/fs/libos_fs_encrypted.c b/libos/src/fs/libos_fs_encrypted.c index 15f73725da..16330c35c7 100644 --- a/libos/src/fs/libos_fs_encrypted.c +++ b/libos/src/fs/libos_fs_encrypted.c @@ -19,6 +19,10 @@ static LISTP_TYPE(libos_encrypted_files_key) g_keys = LISTP_INIT; /* Protects the `g_keys` list, but also individual keys, since they can be updated */ static struct libos_lock g_keys_lock; +static LISTP_TYPE(libos_encrypted_volume) g_volumes = LISTP_INIT; + +/* Protects the `g_volumes` list. */ +static struct libos_lock g_volumes_lock; static pf_status_t cb_read(pf_handle_t handle, void* buffer, uint64_t offset, size_t size) { PAL_HANDLE pal_handle = (PAL_HANDLE)handle; @@ -109,7 +113,7 @@ static pf_status_t cb_aes_cmac(const pf_key_t* key, const void* input, size_t in sizeof(*mac)); if (ret != 0) { log_warning("lib_AESCMAC failed: %d", ret); - return PF_STATUS_CALLBACK_FAILED; + return PF_STATUS_CRYPTO_ERROR; } return PF_STATUS_SUCCESS; } @@ -121,7 +125,7 @@ static pf_status_t cb_aes_gcm_encrypt(const pf_key_t* key, const pf_iv_t* iv, co input_size, aad, aad_size, output, (uint8_t*)mac, sizeof(*mac)); if (ret != 0) { log_warning("lib_AESGCMEncrypt failed: %d", ret); - return PF_STATUS_CALLBACK_FAILED; + return PF_STATUS_CRYPTO_ERROR; } return PF_STATUS_SUCCESS; } @@ -134,7 +138,10 @@ static pf_status_t cb_aes_gcm_decrypt(const pf_key_t* key, const pf_iv_t* iv, co sizeof(*mac)); if (ret != 0) { log_warning("lib_AESGCMDecrypt failed: %d", ret); - return PF_STATUS_CALLBACK_FAILED; + if (ret == PAL_ERROR_CRYPTO_AUTH_FAILED) + return PF_STATUS_MAC_MISMATCH; + else + return PF_STATUS_CRYPTO_ERROR; } return PF_STATUS_SUCCESS; } @@ -154,6 +161,81 @@ static void cb_debug(const char* msg) { } #endif +static int uri_to_normalized_path(const char* uri, char** out_norm_path) { + assert(strstartswith(uri, URI_PREFIX_FILE)); + const char* path = uri + static_strlen(URI_PREFIX_FILE); + + size_t norm_path_size = strlen(path) + 1; + char* norm_path = malloc(norm_path_size); + if (!norm_path) { + return -ENOMEM; + } + + if (!get_norm_path(path, norm_path, &norm_path_size)) { + free(norm_path); + return -EINVAL; + } + + *out_norm_path = norm_path; + return 0; +} + +static void update_mac_in_file_state_map(struct libos_encrypted_file* enc, bool fs_reachable, + const pf_mac_t* closing_root_mac) { + char* norm_path = NULL; + int ret = uri_to_normalized_path(enc->uri, &norm_path); + if (ret < 0) { + log_error("Could not normalize uri %s while updating file state map (ret=%d)", enc->uri, + ret); + } else { + log_debug("map update of %sreachable file '%s' closed with MAC=" MAC_PRINTF_PATTERN, + (fs_reachable ? "" : "un"), norm_path, + MAC_PRINTF_ARGS(*closing_root_mac)); // TODO (MST): remove me eventually? + if (fs_reachable) { + /* note: we only update if reachable in fileystem to prevent file-handles made + * unreachable via unlink or rename to modify state. */ + lock(&(enc->volume->files_state_map_lock)); + struct libos_encrypted_volume_state_map* file_state = NULL; + + HASH_FIND_STR(enc->volume->files_state_map, norm_path, file_state); + assert(file_state != NULL); + if (file_state->state == PF_FILE_STATE_ACTIVE) { + /* note: we do not touch it if earlier we determined this file is in inconsistent + * error state. */ + memcpy(file_state->last_seen_root_mac, *closing_root_mac, sizeof(pf_mac_t)); + } + unlock(&(enc->volume->files_state_map_lock)); + free(norm_path); + } + } +} + +static void update_state_in_file_state_map(struct libos_encrypted_file* enc, bool fs_reachable, + libos_encrypted_file_state_t state) { + char* norm_path = NULL; + int ret = uri_to_normalized_path(enc->uri, &norm_path); + if (ret < 0) { + log_error("Could not normalize uri %s while updating file state map (ret=%d)", enc->uri, + ret); + } else { + log_debug("map update of %sreachable file '%s' to state %s", (fs_reachable ? "" : "un"), + norm_path, + file_state_to_string(state)); // TODO (MST): remove me eventually? + if (fs_reachable) { + /* note: we only update if reachable in fileystem to prevent file-handles made + * unreachable via unlink or rename to modify state. */ + lock(&(enc->volume->files_state_map_lock)); + struct libos_encrypted_volume_state_map* file_state = NULL; + + HASH_FIND_STR(enc->volume->files_state_map, norm_path, file_state); + assert(file_state != NULL); + file_state->state = state; + unlock(&(enc->volume->files_state_map_lock)); + free(norm_path); + } + } +} + /* * The `pal_handle` parameter is used if this is a checkpointed file, and we have received the PAL * handle from the parent process. Note that in this case, it would not be safe to attempt opening @@ -164,7 +246,7 @@ static int encrypted_file_internal_open(struct libos_encrypted_file* enc, PAL_HA assert(!enc->pf); int ret; - char* normpath = NULL; + char* norm_path = NULL; if (!pal_handle) { enum pal_create_mode create_mode = create ? PAL_CREATE_ALWAYS : PAL_CREATE_NEVER; @@ -185,43 +267,122 @@ static int encrypted_file_internal_open(struct libos_encrypted_file* enc, PAL_HA } size_t size = pal_attr.pending_size; - assert(strstartswith(enc->uri, URI_PREFIX_FILE)); - const char* path = enc->uri + static_strlen(URI_PREFIX_FILE); - - size_t normpath_size = strlen(path) + 1; - normpath = malloc(normpath_size); - if (!normpath) { - ret = -ENOMEM; - goto out; - } - - if (!get_norm_path(path, normpath, &normpath_size)) { - ret = -EINVAL; + ret = uri_to_normalized_path(enc->uri, &norm_path); + if (ret < 0) goto out; - } pf_context_t* pf; lock(&g_keys_lock); - if (!enc->key->is_set) { - log_warning("key '%s' is not set", enc->key->name); + if (!enc->volume->key->is_set) { + log_warning("key '%s' is not set", enc->volume->key->name); unlock(&g_keys_lock); ret = -EACCES; goto out; } - pf_status_t pfs = pf_open(pal_handle, normpath, size, PF_FILE_MODE_READ | PF_FILE_MODE_WRITE, - create, &enc->key->pf_key, &pf); + libos_encrypted_file_state_t new_state_in_map = PF_FILE_STATE_ACTIVE; + pf_mac_t opening_root_mac; + pf_status_t pfs = pf_open(pal_handle, norm_path, size, PF_FILE_MODE_READ | PF_FILE_MODE_WRITE, + create, &enc->volume->key->pf_key, &opening_root_mac, &pf); unlock(&g_keys_lock); if (PF_FAILURE(pfs)) { - log_warning("pf_open failed: %s", pf_strerror(pfs)); ret = -EACCES; - goto out; + if (pfs != PF_STATUS_CORRUPTED) { + log_warning("pf_open failed: %s", pf_strerror(pfs)); + goto out; + } + log_error("pf_open of file '%s' encountered corrupted state during open", norm_path); + new_state_in_map = PF_FILE_STATE_ERROR; } - enc->pf = pf; - enc->pal_handle = pal_handle; - ret = 0; + /* rollback protection */ + log_debug("file '%s' opened with MAC=" MAC_PRINTF_PATTERN, norm_path, + MAC_PRINTF_ARGS(opening_root_mac)); // TODO (MST): remove me eventually? + struct libos_encrypted_volume_state_map* file_state = NULL; + lock(&(enc->volume->files_state_map_lock)); + /* - get current state */ + HASH_FIND_STR(enc->volume->files_state_map, norm_path, file_state); + if (new_state_in_map != PF_FILE_STATE_ERROR) { + /* - check current state */ + if (create) { + if (file_state && (file_state->state != PF_FILE_STATE_DELETED)) { + // Note: with create=true we want to open without overwriting, so only valid state + // for an existing map entry is if the file was known to be deleted. + log_error("newly created file '%s' is in state %s", norm_path, + file_state_to_string(file_state->state)); + if (enc->volume->protection_mode != PF_ENCLAVE_LIFE_RB_PROTECTION_NONE) { + pf_close(pf, NULL); + ret = -EEXIST; + new_state_in_map = PF_FILE_STATE_ERROR; + } + } + } else { + if (file_state) { + if ((file_state->state == PF_FILE_STATE_ERROR) || + (file_state->state == PF_FILE_STATE_DELETED)) { + log_error("file '%s' was seen before but in %s state", norm_path, + file_state_to_string(file_state->state)); + if (enc->volume->protection_mode != PF_ENCLAVE_LIFE_RB_PROTECTION_NONE) { + pf_close(pf, NULL); + ret = -EACCES; + new_state_in_map = PF_FILE_STATE_ERROR; + } + } else if (memcmp(file_state->last_seen_root_mac, opening_root_mac, + sizeof(pf_mac_t)) != 0) { + log_error( + "file '%s' was seen before but in different inconsistent (rolled-back?) " + "state, expected MAC=" MAC_PRINTF_PATTERN + " but file had " + "MAC=" MAC_PRINTF_PATTERN, + norm_path, MAC_PRINTF_ARGS(file_state->last_seen_root_mac), + MAC_PRINTF_ARGS(opening_root_mac)); + if (enc->volume->protection_mode != PF_ENCLAVE_LIFE_RB_PROTECTION_NONE) { + pf_close(pf, NULL); + ret = -EACCES; + new_state_in_map = PF_FILE_STATE_ERROR; + } + } + } else { + if (enc->volume->protection_mode == PF_ENCLAVE_LIFE_RB_PROTECTION_STRICT) { + log_error( + "file '%s' was not seen before which is not allowed with strict rollback " + "protection mode", + norm_path); + pf_close(pf, NULL); + ret = -EACCES; + new_state_in_map = PF_FILE_STATE_ERROR; + } + } + } + } + /* - uodate map with new state */ + if (file_state == NULL) { + file_state = malloc(sizeof(struct libos_encrypted_volume_state_map)); + if (file_state == NULL) { + ret = -ENOMEM; + goto out_unlock_map; + } + file_state->norm_path = norm_path; + norm_path = NULL; /* to prevent freeing it */ + HASH_ADD_KEYPTR(hh, enc->volume->files_state_map, file_state->norm_path, + strlen(file_state->norm_path), file_state); + } + /* we do below unconditionally as we might recreate a deleted file or overwrite an existing + * one */ + memcpy(file_state->last_seen_root_mac, opening_root_mac, sizeof(pf_mac_t)); + file_state->state = new_state_in_map; + log_debug("updated file protection map with file '%s', state '%s' and MAC=" MAC_PRINTF_PATTERN, + file_state->norm_path, file_state_to_string(file_state->state), + MAC_PRINTF_ARGS(file_state->last_seen_root_mac)); + + if (ret == 0) { + enc->pf = pf; + enc->pal_handle = pal_handle; + } + +out_unlock_map: + unlock(&(enc->volume->files_state_map_lock)); out: - free(normpath); + free(norm_path); if (ret < 0) PalObjectDestroy(pal_handle); return ret; @@ -245,14 +406,16 @@ int parse_pf_key(const char* key_str, pf_key_t* pf_key) { return 0; } -static void encrypted_file_internal_close(struct libos_encrypted_file* enc) { +static void encrypted_file_internal_close(struct libos_encrypted_file* enc, bool fs_reachable) { assert(enc->pf); - - pf_status_t pfs = pf_close(enc->pf); + pf_mac_t closing_root_mac; + pf_status_t pfs = pf_close(enc->pf, &closing_root_mac); if (PF_FAILURE(pfs)) { log_warning("pf_close failed: %s", pf_strerror(pfs)); + update_state_in_file_state_map(enc, fs_reachable, PF_FILE_STATE_ERROR); + } else { + update_mac_in_file_state_map(enc, fs_reachable, &closing_root_mac); } - enc->pf = NULL; PalObjectDestroy(enc->pal_handle); enc->pal_handle = NULL; @@ -283,6 +446,8 @@ int init_encrypted_files(void) { #endif if (!create_lock(&g_keys_lock)) return -ENOMEM; + if (!create_lock(&g_volumes_lock)) + return -ENOMEM; pf_set_callbacks(&cb_read, &cb_write, &cb_fsync, &cb_truncate, &cb_aes_cmac, &cb_aes_gcm_encrypt, &cb_aes_gcm_decrypt, @@ -445,12 +610,68 @@ void update_encrypted_files_key(struct libos_encrypted_files_key* key, const pf_ unlock(&g_keys_lock); } -static int encrypted_file_alloc(const char* uri, struct libos_encrypted_files_key* key, +static struct libos_encrypted_volume* get_volume(const char* mount_point_path) { + assert(locked(&g_volumes_lock)); + + struct libos_encrypted_volume* volume; + LISTP_FOR_EACH_ENTRY(volume, &g_volumes, list) { + if (!strcmp(volume->mount_point_path, mount_point_path)) { + return volume; + } + } + + return NULL; +} + +int register_encrypted_volume(struct libos_encrypted_volume* volume) { + assert(volume && volume->mount_point_path); + + lock(&g_volumes_lock); + + int ret = 0; + + struct libos_encrypted_volume* existing_volume = get_volume(volume->mount_point_path); + if (existing_volume) { + ret = -EEXIST; + goto out; + } + LISTP_ADD_TAIL(volume, &g_volumes, list); +out: + unlock(&g_volumes_lock); + return ret; +} + +struct libos_encrypted_volume* get_encrypted_volume(const char* mount_point_path) { + lock(&g_volumes_lock); + struct libos_encrypted_volume* volume = get_volume(mount_point_path); + unlock(&g_volumes_lock); + return volume; +} + +int list_encrypted_volumes(int (*callback)(struct libos_encrypted_volume* volume, void* arg), + void* arg) { + lock(&g_volumes_lock); + + int ret; + + struct libos_encrypted_volume* volume; + LISTP_FOR_EACH_ENTRY(volume, &g_volumes, list) { + ret = callback(volume, arg); + if (ret < 0) + goto out; + } + ret = 0; +out: + unlock(&g_volumes_lock); + return ret; +} + +static int encrypted_file_alloc(const char* uri, struct libos_encrypted_volume* volume, struct libos_encrypted_file** out_enc) { assert(strstartswith(uri, URI_PREFIX_FILE)); - if (!key) { - log_debug("trying to open a file (%s) before key is set", uri); + if (!volume) { + log_debug("trying to open a file (%s) before volume is set", uri); return -EACCES; } @@ -458,23 +679,35 @@ static int encrypted_file_alloc(const char* uri, struct libos_encrypted_files_ke if (!enc) return -ENOMEM; + int ret; + enc->uri = NULL; + enc->uri = strdup(uri); if (!enc->uri) { - free(enc); - return -ENOMEM; + ret = -ENOMEM; + goto err; } - enc->key = key; + + enc->volume = volume; enc->use_count = 0; enc->pf = NULL; enc->pal_handle = NULL; *out_enc = enc; return 0; + +err: + if (enc) { + if (enc->uri) + free(enc->uri); + free(enc); + } + return ret; } -int encrypted_file_open(const char* uri, struct libos_encrypted_files_key* key, +int encrypted_file_open(const char* uri, struct libos_encrypted_volume* volume, struct libos_encrypted_file** out_enc) { struct libos_encrypted_file* enc; - int ret = encrypted_file_alloc(uri, key, &enc); + int ret = encrypted_file_alloc(uri, volume, &enc); if (ret < 0) return ret; @@ -489,10 +722,10 @@ int encrypted_file_open(const char* uri, struct libos_encrypted_files_key* key, return 0; } -int encrypted_file_create(const char* uri, mode_t perm, struct libos_encrypted_files_key* key, +int encrypted_file_create(const char* uri, mode_t perm, struct libos_encrypted_volume* volume, struct libos_encrypted_file** out_enc) { struct libos_encrypted_file* enc; - int ret = encrypted_file_alloc(uri, key, &enc); + int ret = encrypted_file_alloc(uri, volume, &enc); if (ret < 0) return ret; @@ -529,28 +762,30 @@ int encrypted_file_get(struct libos_encrypted_file* enc) { return 0; } -void encrypted_file_put(struct libos_encrypted_file* enc) { +void encrypted_file_put(struct libos_encrypted_file* enc, bool fs_reachable) { assert(enc->use_count > 0); assert(enc->pf); enc->use_count--; if (enc->use_count == 0) { - encrypted_file_internal_close(enc); + encrypted_file_internal_close(enc, fs_reachable); } } -int encrypted_file_flush(struct libos_encrypted_file* enc) { +int encrypted_file_flush(struct libos_encrypted_file* enc, bool fs_reachable) { assert(enc->pf); pf_status_t pfs = pf_flush(enc->pf); if (PF_FAILURE(pfs)) { log_warning("pf_flush failed: %s", pf_strerror(pfs)); + if (pfs == PF_STATUS_CORRUPTED) + update_state_in_file_state_map(enc, fs_reachable, PF_FILE_STATE_ERROR); return -EACCES; } return 0; } int encrypted_file_read(struct libos_encrypted_file* enc, void* buf, size_t buf_size, - file_off_t offset, size_t* out_count) { + file_off_t offset, size_t* out_count, bool fs_reachable) { assert(enc->pf); if (offset < 0) @@ -562,6 +797,8 @@ int encrypted_file_read(struct libos_encrypted_file* enc, void* buf, size_t buf_ pf_status_t pfs = pf_read(enc->pf, offset, buf_size, buf, &count); if (PF_FAILURE(pfs)) { log_warning("pf_read failed: %s", pf_strerror(pfs)); + if (pfs == PF_STATUS_CORRUPTED) + update_state_in_file_state_map(enc, fs_reachable, PF_FILE_STATE_ERROR); return -EACCES; } *out_count = count; @@ -569,7 +806,7 @@ int encrypted_file_read(struct libos_encrypted_file* enc, void* buf, size_t buf_ } int encrypted_file_write(struct libos_encrypted_file* enc, const void* buf, size_t buf_size, - file_off_t offset, size_t* out_count) { + file_off_t offset, size_t* out_count, bool fs_reachable) { assert(enc->pf); if (offset < 0) @@ -580,6 +817,8 @@ int encrypted_file_write(struct libos_encrypted_file* enc, const void* buf, size pf_status_t pfs = pf_write(enc->pf, offset, buf_size, buf); if (PF_FAILURE(pfs)) { log_warning("pf_write failed: %s", pf_strerror(pfs)); + if (pfs == PF_STATUS_CORRUPTED) + update_state_in_file_state_map(enc, fs_reachable, PF_FILE_STATE_ERROR); return -EACCES; } /* We never write less than `buf_size` */ @@ -587,13 +826,16 @@ int encrypted_file_write(struct libos_encrypted_file* enc, const void* buf, size return 0; } -int encrypted_file_get_size(struct libos_encrypted_file* enc, file_off_t* out_size) { +int encrypted_file_get_size(struct libos_encrypted_file* enc, file_off_t* out_size, + bool fs_reachable) { assert(enc->pf); uint64_t size; pf_status_t pfs = pf_get_size(enc->pf, &size); if (PF_FAILURE(pfs)) { log_warning("pf_get_size failed: %s", pf_strerror(pfs)); + if (pfs == PF_STATUS_CORRUPTED) + update_state_in_file_state_map(enc, fs_reachable, PF_FILE_STATE_ERROR); return -EACCES; } if (OVERFLOWS(file_off_t, size)) @@ -602,7 +844,7 @@ int encrypted_file_get_size(struct libos_encrypted_file* enc, file_off_t* out_si return 0; } -int encrypted_file_set_size(struct libos_encrypted_file* enc, file_off_t size) { +int encrypted_file_set_size(struct libos_encrypted_file* enc, file_off_t size, bool fs_reachable) { assert(enc->pf); if (size < 0) @@ -613,6 +855,8 @@ int encrypted_file_set_size(struct libos_encrypted_file* enc, file_off_t size) { pf_status_t pfs = pf_set_size(enc->pf, size); if (PF_FAILURE(pfs)) { log_warning("pf_set_size failed: %s", pf_strerror(pfs)); + if (pfs == PF_STATUS_CORRUPTED) + update_state_in_file_state_map(enc, fs_reachable, PF_FILE_STATE_ERROR); return -EACCES; } return 0; @@ -621,34 +865,26 @@ int encrypted_file_set_size(struct libos_encrypted_file* enc, file_off_t size) { int encrypted_file_rename(struct libos_encrypted_file* enc, const char* new_uri) { assert(enc->pf); - int ret; - char* new_normpath = NULL; - char* new_uri_copy = strdup(new_uri); if (!new_uri_copy) return -ENOMEM; - assert(strstartswith(enc->uri, URI_PREFIX_FILE)); - const char* old_path = enc->uri + static_strlen(URI_PREFIX_FILE); - - assert(strstartswith(new_uri, URI_PREFIX_FILE)); - const char* new_path = new_uri + static_strlen(URI_PREFIX_FILE); - - size_t new_normpath_size = strlen(new_path) + 1; - new_normpath = malloc(new_normpath_size); - if (!new_normpath) { - ret = -ENOMEM; + int ret; + char* new_norm_path = NULL; + char* old_norm_path = NULL; + ret = uri_to_normalized_path(enc->uri, &old_norm_path); + if (ret < 0) goto out; - } - - if (!get_norm_path(new_path, new_normpath, &new_normpath_size)) { - ret = -EINVAL; + ret = uri_to_normalized_path(new_uri, &new_norm_path); + if (ret < 0) goto out; - } - pf_status_t pfs = pf_rename(enc->pf, new_normpath); + pf_mac_t new_root_mac; + pf_status_t pfs = pf_rename(enc->pf, new_norm_path, &new_root_mac); if (PF_FAILURE(pfs)) { log_warning("pf_rename failed: %s", pf_strerror(pfs)); + if (pfs == PF_STATUS_CORRUPTED) + update_state_in_file_state_map(enc, /* fs_reachable */ true, PF_FILE_STATE_ERROR); ret = -EACCES; goto out; } @@ -658,27 +894,77 @@ int encrypted_file_rename(struct libos_encrypted_file* enc, const char* new_uri) log_warning("PalStreamChangeName failed: %s", pal_strerror(ret)); /* We failed to rename the file. Try to restore the name in header. */ - pfs = pf_rename(enc->pf, old_path); + pfs = pf_rename(enc->pf, old_norm_path, &new_root_mac); if (PF_FAILURE(pfs)) { log_warning("pf_rename (during cleanup) failed, the file might be unusable: %s", pf_strerror(pfs)); + if (pfs == PF_STATUS_CORRUPTED) + update_state_in_file_state_map(enc, /* fs_reachable */ true, PF_FILE_STATE_ERROR); } - + old_norm_path = NULL; // don't free it later ... ret = pal_to_unix_errno(ret); goto out; } + /* update file state map */ + log_debug("file '%s' renamed to '%s' with MAC=" MAC_PRINTF_PATTERN, old_norm_path, + new_norm_path, + MAC_PRINTF_ARGS(new_root_mac)); // TODO (MST): remove me eventually? + lock(&(enc->volume->files_state_map_lock)); + struct libos_encrypted_volume_state_map* old_file_state = NULL; + HASH_FIND_STR(enc->volume->files_state_map, old_norm_path, old_file_state); + assert(old_file_state != NULL); + struct libos_encrypted_volume_state_map* new_file_state = NULL; + HASH_FIND_STR(enc->volume->files_state_map, new_norm_path, new_file_state); + if (new_file_state == NULL) { + new_file_state = malloc(sizeof(struct libos_encrypted_volume_state_map)); + if (new_file_state == NULL) { + ret = -ENOMEM; + goto out; + } + new_file_state->norm_path = new_norm_path; + HASH_ADD_KEYPTR(hh, enc->volume->files_state_map, new_file_state->norm_path, + strlen(new_file_state->norm_path), new_file_state); + } else { + free(new_norm_path); /* should be same as old one used during HASH_ADD */ + new_norm_path = new_file_state->norm_path; + } + new_file_state->state = old_file_state->state; + memcpy(new_file_state->last_seen_root_mac, new_root_mac, sizeof(pf_mac_t)); + old_file_state->state = PF_FILE_STATE_DELETED; /* note: this might remove error state from that + file but that is fine as it is deleted now. */ + memset(old_file_state->last_seen_root_mac, 0, sizeof(pf_mac_t)); + unlock(&(enc->volume->files_state_map_lock)); free(enc->uri); - enc->uri = new_uri_copy; - new_uri_copy = NULL; + enc->uri = new_uri_copy; + new_uri_copy = NULL; + new_norm_path = NULL; + ret = 0; out: - free(new_normpath); + if (ret) { + // store in file state map fact that we could not rename file properly + if (!locked(&(enc->volume->files_state_map_lock))) // for OOM case from above! + lock(&(enc->volume->files_state_map_lock)); + if (old_file_state == NULL) // we might already have it! + HASH_FIND_STR(enc->volume->files_state_map, old_norm_path, old_file_state); + assert(old_file_state != NULL); + old_file_state->state = PF_FILE_STATE_ERROR; + pf_set_corrupted(enc->pf); + unlock(&(enc->volume->files_state_map_lock)); + } + free(old_norm_path); + free(new_norm_path); free(new_uri_copy); return ret; } +int encrypted_file_unlink(struct libos_encrypted_file* enc) { + update_state_in_file_state_map(enc, /* fs_reachable */ true, PF_FILE_STATE_DELETED); + return 0; +} + /* Checkpoint the `g_keys` list. */ BEGIN_CP_FUNC(all_encrypted_files_keys) { __UNUSED(size); @@ -746,6 +1032,89 @@ BEGIN_RS_FUNC(encrypted_files_key) { } END_RS_FUNC(encrypted_files_key) +/* Checkpoint the `g_volumes` list. Note we only call this to checkpoint all volumes. The list + * itself is not checkpointed (and hence also no corresponding restore function). The list is + * reconstructed in the restore function of the volumes itself. */ +BEGIN_CP_FUNC(all_encrypted_volumes) { + __UNUSED(size); + __UNUSED(obj); + __UNUSED(objp); + + lock(&g_volumes_lock); + struct libos_encrypted_volume* volume; + LISTP_FOR_EACH_ENTRY(volume, &g_volumes, list) { + DO_CP(encrypted_volume, volume, /*objp=*/NULL); + } + unlock(&g_volumes_lock); +} +END_CP_FUNC_NO_RS(all_encrypted_volumes) + +BEGIN_CP_FUNC(encrypted_volume) { + __UNUSED(size); + + struct libos_encrypted_volume* volume = obj; + struct libos_encrypted_volume* new_volume = NULL; + + size_t off = GET_FROM_CP_MAP(obj); + if (!off) { /* We haven't already checkpointed this volume */ + off = ADD_CP_OFFSET(sizeof(struct libos_encrypted_volume)); + ADD_TO_CP_MAP(obj, off); + new_volume = (struct libos_encrypted_volume*)(base + off); + + log_debug("CP(encrypted_volume): mount_point_path=%s protection_mode=%d file_state_mape=%p", + volume->mount_point_path, volume->protection_mode, + volume->files_state_map); // TODO (MST): remove me eventually? + DO_CP_MEMBER(str, volume, new_volume, mount_point_path); + new_volume->protection_mode = volume->protection_mode; + lock(&volume->files_state_map_lock); + /* Note: for now we do not serialize hashmap so just make sure it is treated as empty list. + * Serialization would cover some corner cases, e.g., `send_handle_enc` test case might work + * in strict and not only in non-strict mode. However, checkpoint/restore with current + * framework does not provide a low-hanging fruit. For other reasons (persistant rollback + * protection) we will need file-based (de)serialization and so could use that here. + * However, to really solve multi-processor case, we have to adopt the same strategy as for + * file locks, i.e., a leader-based centralized map and IPC to access/modify. Hence, no + * point in doing some complicated interim throw-away variant. */ + new_volume->files_state_map = NULL; + unlock(&volume->files_state_map_lock); + /* files_state_map_lock has no check point, it will be recreated in restore */ + lock(&g_keys_lock); + DO_CP_MEMBER(encrypted_files_key, volume, new_volume, key); + unlock(&g_keys_lock); + INIT_LIST_HEAD(new_volume, list); + + ADD_CP_FUNC_ENTRY(off); + } else { + new_volume = (struct libos_encrypted_volume*)(base + off); + } + if (objp) + *objp = (void*)new_volume; +} +END_CP_FUNC(encrypted_volume) + +BEGIN_RS_FUNC(encrypted_volume) { + __UNUSED(offset); + struct libos_encrypted_volume* migrated_volume = (void*)(base + GET_CP_FUNC_ENTRY()); + + CP_REBASE(migrated_volume->mount_point_path); + + /* protection_mode needs no restore action. */ + /* files_state_map for now is not serialized but just an empty list, so no restore action + * needed. See above in checkpoint for more information. */ + if (!create_lock(&migrated_volume->files_state_map_lock)) { + return -ENOMEM; + } + CP_REBASE(migrated_volume->key); + log_debug("RS(encrypted_volume): mount_point_path=%s protection_mode=%d file_state_mape=%p", + migrated_volume->mount_point_path, migrated_volume->protection_mode, + migrated_volume->files_state_map); // TODO (MST): remove me eventually? + + int ret = register_encrypted_volume(migrated_volume); + if (ret < 0) + return ret; +} +END_RS_FUNC(encrypted_volume) + BEGIN_CP_FUNC(encrypted_file) { __UNUSED(size); @@ -753,7 +1122,7 @@ BEGIN_CP_FUNC(encrypted_file) { struct libos_encrypted_file* new_enc = NULL; if (enc->pf) { - int ret = encrypted_file_flush(enc); + int ret = encrypted_file_flush(enc, true); if (ret < 0) return ret; } @@ -764,9 +1133,7 @@ BEGIN_CP_FUNC(encrypted_file) { new_enc->use_count = enc->use_count; DO_CP_MEMBER(str, enc, new_enc, uri); - lock(&g_keys_lock); - DO_CP_MEMBER(encrypted_files_key, enc, new_enc, key); - unlock(&g_keys_lock); + DO_CP_MEMBER(encrypted_volume, enc, new_enc, volume); /* `enc->pf` will be recreated during restore */ new_enc->pf = NULL; @@ -788,7 +1155,8 @@ BEGIN_RS_FUNC(encrypted_file) { __UNUSED(offset); CP_REBASE(enc->uri); - CP_REBASE(enc->key); + + CP_REBASE(enc->volume); /* If the file was used, recreate `enc->pf` based on the PAL handle */ assert(!enc->pf); diff --git a/libos/src/fs/libos_fs_pseudo.c b/libos/src/fs/libos_fs_pseudo.c index eab8129e0f..7a0c19f807 100644 --- a/libos/src/fs/libos_fs_pseudo.c +++ b/libos/src/fs/libos_fs_pseudo.c @@ -266,6 +266,9 @@ static int pseudo_stat(struct libos_dentry* dent, struct stat* buf) { } static int pseudo_hstat(struct libos_handle* handle, struct stat* buf) { + /* Note: derefence handle->dentry in general has to be protected by handle->lock as it could + * change due to rename. However, as pseudo-fs does not support rename we can safely omit it + * here (or push it on the numerous callers of fs_op->hstat). */ return pseudo_istat(handle->dentry, handle->inode, buf); } diff --git a/libos/src/fs/libos_namei.c b/libos/src/fs/libos_namei.c index f23f2de8da..315735d10c 100644 --- a/libos/src/fs/libos_namei.c +++ b/libos/src/fs/libos_namei.c @@ -367,7 +367,7 @@ static void assoc_handle_with_dentry(struct libos_handle* hdl, struct libos_dent assert(locked(&g_dcache_lock)); assert(dent->inode); - hdl->dentry = dent; + hdl->dentry = dent; /* not-yet-shared handle, so no look needed. */ get_dentry(dent); hdl->inode = dent->inode; @@ -381,7 +381,7 @@ static void assoc_handle_with_dentry(struct libos_handle* hdl, struct libos_dent int dentry_open(struct libos_handle* hdl, struct libos_dentry* dent, int flags) { assert(locked(&g_dcache_lock)); assert(dent->inode); - assert(!hdl->dentry); + assert(!hdl->dentry); /* not-yet-shared handle, so no look needed. */ int ret; struct libos_fs* fs = dent->inode->fs; @@ -431,7 +431,7 @@ int open_namei(struct libos_handle* hdl, struct libos_dentry* start, const char* assert(hdl); if (hdl) - assert(!hdl->dentry); + assert(!hdl->dentry); /* not-yet-shared handle, so no look needed. */ lock(&g_dcache_lock); @@ -741,8 +741,10 @@ int get_dirfd_dentry(int dirfd, struct libos_dentry** dir) { return -ENOTDIR; } + lock(&hdl->lock); /* while hdl->is_dir is immutable, hdl->dentry can change due to rename */ get_dentry(hdl->dentry); *dir = hdl->dentry; + unlock(&hdl->lock); put_handle(hdl); return 0; } diff --git a/libos/src/fs/proc/thread.c b/libos/src/fs/proc/thread.c index c3da147c48..b22828fbb9 100644 --- a/libos/src/fs/proc/thread.c +++ b/libos/src/fs/proc/thread.c @@ -29,9 +29,11 @@ int proc_thread_follow_link(struct libos_dentry* dent, char** out_target) { dent = g_process.cwd; get_dentry(dent); } else if (strcmp(name, "exe") == 0) { + lock(&g_process.exec->lock); dent = g_process.exec->dentry; if (dent) get_dentry(dent); + unlock(&g_process.exec->lock); } unlock(&g_process.fs_lock); @@ -91,11 +93,13 @@ int proc_thread_maps_load(struct libos_dentry* dent, char** out_data, size_t* ou retry_emit_vma: if (vma->file) { int dev_major = 0, dev_minor = 0; + lock(&vma->file->lock); unsigned long ino = vma->file->dentry ? dentry_ino(vma->file->dentry) : 0; char* path = NULL; if (vma->file->dentry) dentry_abs_path(vma->file->dentry, &path, /*size=*/NULL); + unlock(&vma->file->lock); EMIT(ADDR_FMT(start), start); EMIT("-"); @@ -310,6 +314,7 @@ int proc_thread_fd_follow_link(struct libos_dentry* dent, char** out_target) { int ret; struct libos_handle* hdl = handle_map->map[fd]->handle; + lock(&hdl->lock); if (hdl->dentry) { ret = dentry_abs_path(hdl->dentry, out_target, /*size=*/NULL); } else { @@ -318,6 +323,7 @@ int proc_thread_fd_follow_link(struct libos_dentry* dent, char** out_target) { *out_target = describe_handle(hdl); ret = *out_target ? 0 : -ENOMEM; } + unlock(&hdl->lock); rwlock_read_unlock(&handle_map->lock); @@ -457,9 +463,11 @@ int proc_thread_stat_load(struct libos_dentry* dent, char** out_data, size_t* ou char comm[16] = {0}; lock(&g_process.fs_lock); + lock(&g_process.exec->lock); size_t name_length = g_process.exec->dentry->name_len; memcpy(comm, g_process.exec->dentry->name, name_length > sizeof(comm) - 1 ? sizeof(comm) - 1 : name_length); + unlock(&g_process.exec->lock); unlock(&g_process.fs_lock); size_t virtual_mem_size = get_total_memory_usage(); diff --git a/libos/src/ipc/libos_ipc_process_info.c b/libos/src/ipc/libos_ipc_process_info.c index 77767903f9..c3dadb48fc 100644 --- a/libos/src/ipc/libos_ipc_process_info.c +++ b/libos/src/ipc/libos_ipc_process_info.c @@ -101,8 +101,11 @@ int ipc_pid_getmeta_callback(IDTYPE src, void* msg_data, uint64_t seq) { struct libos_dentry* dent = NULL; switch (msgin->code) { case PID_META_EXEC: - if (g_process.exec) + if (g_process.exec) { + lock(&g_process.exec->lock); dent = g_process.exec->dentry; + unlock(&g_process.exec->lock); + } break; case PID_META_CWD: dent = g_process.cwd; diff --git a/libos/src/libos_rtld.c b/libos/src/libos_rtld.c index baa08f9e30..5a86dd7899 100644 --- a/libos/src/libos_rtld.c +++ b/libos/src/libos_rtld.c @@ -597,10 +597,12 @@ static int load_and_check_shebang(struct libos_handle* file, const char* pathnam ret = read_partial_fragment(file, shebang, sizeof(shebang), /*offset=*/0, &shebang_len); if (ret < 0 || shebang_len < 2 || shebang[0] != '#' || shebang[1] != '!') { char* path = NULL; + lock(&file->lock); if (file->dentry) { /* this may fail, but we are already inside a more serious error handler */ dentry_abs_path(file->dentry, &path, /*size=*/NULL); } + unlock(&file->lock); log_debug("Failed to read shebang line from %s", path ? path : "(unknown)"); free(path); return -ENOEXEC; diff --git a/libos/src/meson.build b/libos/src/meson.build index b9946bc2af..43e19095c2 100644 --- a/libos/src/meson.build +++ b/libos/src/meson.build @@ -19,6 +19,7 @@ libos_sources = files( 'fs/chroot/fs.c', 'fs/dev/attestation.c', 'fs/dev/fs.c', + 'fs/dev/rollback.c', 'fs/etc/fs.c', 'fs/eventfd/fs.c', 'fs/libos_dcache.c', diff --git a/libos/src/sys/libos_clone.c b/libos/src/sys/libos_clone.c index f8636a0428..da37d3143a 100644 --- a/libos/src/sys/libos_clone.c +++ b/libos/src/sys/libos_clone.c @@ -95,6 +95,7 @@ static BEGIN_MIGRATION_DEF(fork, struct libos_process* process_description, struct libos_ipc_ids* process_ipc_ids) { DEFINE_MIGRATE(process_ipc_ids, process_ipc_ids, sizeof(*process_ipc_ids)); DEFINE_MIGRATE(all_encrypted_files_keys, NULL, 0); + DEFINE_MIGRATE(all_encrypted_volumes, NULL, 0); DEFINE_MIGRATE(dentry_root, NULL, 0); DEFINE_MIGRATE(all_mounts, NULL, 0); DEFINE_MIGRATE(all_vmas, NULL, 0); diff --git a/libos/src/sys/libos_fcntl.c b/libos/src/sys/libos_fcntl.c index 24578155e2..0fa6b7b297 100644 --- a/libos/src/sys/libos_fcntl.c +++ b/libos/src/sys/libos_fcntl.c @@ -199,29 +199,34 @@ long libos_syscall_fcntl(int fd, int cmd, unsigned long arg) { break; } - if (!hdl->dentry) { + lock(&hdl->lock); + struct libos_dentry* dent = hdl->dentry; + + if (!dent) { /* TODO: Linux allows locks on pipes etc. Our locks work only for "normal" files * that have a dentry. */ ret = -EINVAL; - break; + goto out_setlkw_unlock; } if (fl->l_type == F_RDLCK && !(hdl->acc_mode & MAY_READ)) { ret = -EINVAL; - break; + goto out_setlkw_unlock; } if (fl->l_type == F_WRLCK && !(hdl->acc_mode & MAY_WRITE)) { ret = -EINVAL; - break; + goto out_setlkw_unlock; } struct libos_file_lock file_lock; ret = flock_to_file_lock(fl, hdl, &file_lock); if (ret < 0) - break; + goto out_setlkw_unlock; - ret = file_lock_set(hdl->dentry, &file_lock, /*wait=*/cmd == F_SETLKW); + ret = file_lock_set(dent, &file_lock, /*wait=*/cmd == F_SETLKW); + out_setlkw_unlock: + unlock(&hdl->lock); break; } @@ -234,9 +239,12 @@ long libos_syscall_fcntl(int fd, int cmd, unsigned long arg) { break; } - if (!hdl->dentry) { + lock(&hdl->lock); + struct libos_dentry* dent = hdl->dentry; + + if (!dent) { ret = -EINVAL; - break; + goto out_getlkw_unlock; } struct libos_file_lock file_lock; @@ -246,13 +254,13 @@ long libos_syscall_fcntl(int fd, int cmd, unsigned long arg) { if (file_lock.type == F_UNLCK) { ret = -EINVAL; - break; + goto out_getlkw_unlock; } struct libos_file_lock file_lock2; - ret = file_lock_get(hdl->dentry, &file_lock, &file_lock2); + ret = file_lock_get(dent, &file_lock, &file_lock2); if (ret < 0) - break; + goto out_getlkw_unlock; fl->l_type = file_lock2.type; if (file_lock2.type != F_UNLCK) { @@ -267,6 +275,8 @@ long libos_syscall_fcntl(int fd, int cmd, unsigned long arg) { fl->l_pid = file_lock2.pid; } ret = 0; + out_getlkw_unlock: + unlock(&hdl->lock); break; } @@ -342,7 +352,10 @@ long libos_syscall_flock(unsigned int fd, unsigned int cmd) { .type = lock_type, .handle_id = hdl->id, }; + + lock(&hdl->lock); ret = file_lock_set(hdl->dentry, &file_lock, !(cmd & LOCK_NB)); + unlock(&hdl->lock); out: put_handle(hdl); return ret; diff --git a/libos/src/sys/libos_file.c b/libos/src/sys/libos_file.c index f4050813d3..70c6a8f0a6 100644 --- a/libos/src/sys/libos_file.c +++ b/libos/src/sys/libos_file.c @@ -347,8 +347,31 @@ static int do_rename(struct libos_dentry* old_dent, struct libos_dentry* new_den if (new_dent->inode) put_inode(new_dent->inode); + new_dent->inode = old_dent->inode; old_dent->inode = NULL; + + /* also update dentry of any potentially open fd pointing to old_dent */ + struct libos_handle_map* handle_map = get_thread_handle_map(NULL); + assert(handle_map != NULL); + rwlock_read_lock(&handle_map->lock); + + for (uint32_t i = 0; handle_map->fd_top != FD_NULL && i <= handle_map->fd_top; i++) { + struct libos_fd_handle* fd_handle = handle_map->map[i]; + if (!HANDLE_ALLOCATED(fd_handle)) + continue; + struct libos_handle* handle = fd_handle->handle; + /* see comment in libos_handle.h on loocking strategy protecting handle->lock */ + assert(locked(&g_dcache_lock)); + lock(&handle->lock); + if ((handle->dentry == old_dent) && (handle->inode == new_dent->inode)) { + handle->dentry = new_dent; + put_dentry(old_dent); + get_dentry(new_dent); + } + unlock(&handle->lock); + } + rwlock_read_unlock(&handle_map->lock); return 0; } diff --git a/libos/src/sys/libos_getcwd.c b/libos/src/sys/libos_getcwd.c index 2cd64fca7f..14ae545a7e 100644 --- a/libos/src/sys/libos_getcwd.c +++ b/libos/src/sys/libos_getcwd.c @@ -82,26 +82,35 @@ long libos_syscall_fchdir(int fd) { if (!hdl) return -EBADF; + int ret = 0; + lock(&g_dcache_lock); /* for dent->inode */ + lock(&g_process.fs_lock); /* for g_process.cwd */ + lock(&hdl->lock); /* for hdl->dentry */ + struct libos_dentry* dent = hdl->dentry; if (!dent) { log_debug("FD=%d has no path in the filesystem", fd); - return -ENOTDIR; + ret = -ENOTDIR; + goto out; } if (!dent->inode || dent->inode->type != S_IFDIR) { char* path = NULL; dentry_abs_path(dent, &path, /*size=*/NULL); log_debug("%s is not a directory", path); free(path); - put_handle(hdl); - return -ENOTDIR; + ret = -ENOTDIR; + goto out; } - lock(&g_process.fs_lock); get_dentry(dent); put_dentry(g_process.cwd); g_process.cwd = dent; - unlock(&g_process.fs_lock); + +out: put_handle(hdl); - return 0; + unlock(&hdl->lock); + unlock(&g_process.fs_lock); + unlock(&g_dcache_lock); + return ret; } diff --git a/libos/src/sys/libos_ioctl.c b/libos/src/sys/libos_ioctl.c index 89d5424da9..26c7e08510 100644 --- a/libos/src/sys/libos_ioctl.c +++ b/libos/src/sys/libos_ioctl.c @@ -41,9 +41,11 @@ long libos_syscall_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg) { if (!hdl) return -EBADF; - lock(&g_dcache_lock); + lock(&g_dcache_lock); /* for dentry->inode */ + lock(&hdl->lock); /* for hdl->dentry */ bool is_host_dev = hdl->type == TYPE_CHROOT && hdl->dentry->inode && hdl->dentry->inode->type == S_IFCHR; + unlock(&hdl->lock); unlock(&g_dcache_lock); if (is_host_dev) { diff --git a/libos/src/sys/libos_open.c b/libos/src/sys/libos_open.c index 83fde52c47..069b19b2f4 100644 --- a/libos/src/sys/libos_open.c +++ b/libos/src/sys/libos_open.c @@ -373,15 +373,14 @@ static ssize_t do_getdents(int fd, uint8_t* buf, size_t buf_size, bool is_getden goto out_no_unlock; } + lock(&g_dcache_lock); /* for dentry->inode */ + maybe_lock_pos_handle(hdl); + lock(&hdl->lock); /* for hdl->dentry */ if (!hdl->dentry->inode) { ret = -ENOENT; - goto out_no_unlock; + goto out; } - lock(&g_dcache_lock); - maybe_lock_pos_handle(hdl); - lock(&hdl->lock); - struct libos_dir_handle* dirhdl = &hdl->dir_info; if ((ret = populate_directory_handle(hdl)) < 0) goto out; diff --git a/libos/src/sys/libos_pipe.c b/libos/src/sys/libos_pipe.c index 53af9d5e92..bb677b6f1b 100644 --- a/libos/src/sys/libos_pipe.c +++ b/libos/src/sys/libos_pipe.c @@ -246,14 +246,14 @@ long libos_syscall_mknodat(int dirfd, const char* pathname, mode_t mode, dev_t d hdl1->flags = O_RDONLY; hdl1->acc_mode = MAY_READ; get_dentry(dent); - hdl1->dentry = dent; + hdl1->dentry = dent; /* new not-yet-exported handle, so skip unnecessary handle locking */ hdl2->type = TYPE_PIPE; hdl2->fs = &fifo_builtin_fs; hdl2->flags = O_WRONLY; hdl2->acc_mode = MAY_WRITE; get_dentry(dent); - hdl2->dentry = dent; + hdl2->dentry = dent; /* new not-yet-exported handle, so skip unnecessary handle locking */ /* FIFO must be open'ed to start read/write operations, mark as not ready */ hdl1->info.pipe.ready_for_ops = false; diff --git a/libos/src/sys/libos_stat.c b/libos/src/sys/libos_stat.c index 5e54c0ad5c..13f8200628 100644 --- a/libos/src/sys/libos_stat.c +++ b/libos/src/sys/libos_stat.c @@ -39,15 +39,15 @@ static int do_hstat(struct libos_handle* hdl, struct stat* stat) { if (!fs || !fs->fs_ops || !fs->fs_ops->hstat) return -EACCES; + lock(&hdl->lock); int ret = fs->fs_ops->hstat(hdl, stat); - if (ret < 0) - return ret; - - /* Update `st_ino` from dentry */ - if (hdl->dentry) + if (ret >= 0 && hdl->dentry) { + /* Update `st_ino` from dentry */ stat->st_ino = dentry_ino(hdl->dentry); + } + unlock(&hdl->lock); - return 0; + return ret; } long libos_syscall_stat(const char* file, struct stat* stat) { @@ -210,7 +210,9 @@ long libos_syscall_fstatfs(int fd, struct statfs* buf) { if (!hdl) return -EBADF; + lock(&hdl->lock); struct libos_mount* mount = hdl->dentry ? hdl->dentry->mount : NULL; + unlock(&hdl->lock); put_handle(hdl); return __do_statfs(mount, buf); } diff --git a/libos/src/utils/log.c b/libos/src/utils/log.c index 457ea8897d..777aedaa30 100644 --- a/libos/src/utils/log.c +++ b/libos/src/utils/log.c @@ -34,12 +34,14 @@ void log_setprefix(libos_tcb_t* tcb) { const char* exec_name; if (g_process.exec) { + lock(&g_process.exec->lock); if (g_process.exec->dentry) { exec_name = g_process.exec->dentry->name; } else { /* Unknown executable name */ exec_name = "?"; } + unlock(&g_process.exec->lock); } else { /* `g_process.exec` not available yet, happens on process init */ exec_name = ""; diff --git a/libos/test/fs/conftest.py b/libos/test/fs/conftest.py new file mode 100644 index 0000000000..bfe6c7e0e5 --- /dev/null +++ b/libos/test/fs/conftest.py @@ -0,0 +1,10 @@ +import pytest + +option = None + +def pytest_addoption(parser): + parser.addoption("--skip-teardown", action='store_true') + +def pytest_configure(config): + global option + option = config.option diff --git a/libos/test/fs/gdb_helper.py b/libos/test/fs/gdb_helper.py new file mode 100644 index 0000000000..d145b1b91f --- /dev/null +++ b/libos/test/fs/gdb_helper.py @@ -0,0 +1,25 @@ +import gdb +import re + +def adversary_do(cmd): + # extract interesting info from context + test_function=gdb.selected_frame().older().name() + operation=gdb.selected_frame().name() + internal_path=gdb.selected_frame().read_var('path').string() + external_path=re.sub(r'/tmp/enc_input/', './tmp/enc_input/', internal_path) + external_path_saved=external_path+"._saved_" + try: + internal_path2=gdb.selected_frame().read_var('path2').string() + external_path2=re.sub(r'/tmp/enc_input/', './tmp/enc_input/', internal_path2) + opt_arg=f",{internal_path2}" + except ValueError: + internal_path2="" + external_path2="" + opt_arg="" + + # execute and report result for pytest digestion + try: + cmd(external_path, external_path_saved, external_path2) + print(f"OK: {test_function} in {operation}({internal_path}{opt_arg}])") + except Exception as e: + print(f"FAIL: {test_function} in {operation}({internal_path}{opt_arg}): {e}") diff --git a/libos/test/fs/manifest.template b/libos/test/fs/manifest.template index 612873c068..65d2ec8c1f 100644 --- a/libos/test/fs/manifest.template +++ b/libos/test/fs/manifest.template @@ -10,10 +10,18 @@ fs.mounts = [ { path = "/usr/{{ arch_libdir }}", uri = "file:/usr/{{ arch_libdir }}" }, { path = "/mounted", uri = "file:tmp" }, - { type = "encrypted", path = "/tmp/enc_input", uri = "file:tmp/enc_input" }, - { type = "encrypted", path = "/tmp/enc_output", uri = "file:tmp/enc_output" }, - { type = "encrypted", path = "/mounted/enc_input", uri = "file:tmp/enc_input" }, - { type = "encrypted", path = "/mounted/enc_output", uri = "file:tmp/enc_output" }, +{% if entrypoint in [ "open_close", "seek_tell", "delete", "stat", "truncate", "copy_whole", "copy_seq", "copy_rev", "copy_sendfile", "copy_mmap_whole", "copy_mmap_seq", "copy_mmap_rev", "copy_mounted" ] %} + {# these tests get already-encrypted files as input, so do not work in strict mode until we perists volumes .. #} + { type = "encrypted", protection_mode = "non-strict", path = "/tmp/enc_input", uri = "file:tmp/enc_input" }, + { type = "encrypted", protection_mode = "non-strict", path = "/tmp/enc_output", uri = "file:tmp/enc_output" }, + { type = "encrypted", protection_mode = "non-strict", path = "/mounted/enc_input", uri = "file:tmp/enc_input" }, + { type = "encrypted", protection_mode = "non-strict", path = "/mounted/enc_output", uri = "file:tmp/enc_output" }, +{% else %} + { type = "encrypted", protection_mode = "strict", path = "/tmp/enc_input", uri = "file:tmp/enc_input" }, + { type = "encrypted", protection_mode = "strict", path = "/tmp/enc_output", uri = "file:tmp/enc_output" }, + { type = "encrypted", protection_mode = "strict", path = "/mounted/enc_input", uri = "file:tmp/enc_input" }, + { type = "encrypted", protection_mode = "strict", path = "/mounted/enc_output", uri = "file:tmp/enc_output" }, +{% endif %} { type = "tmpfs", path = "/mnt-tmpfs" }, ] diff --git a/libos/test/fs/meson.build b/libos/test/fs/meson.build index ce3f8b2e90..8bffd67155 100644 --- a/libos/test/fs/meson.build +++ b/libos/test/fs/meson.build @@ -42,6 +42,8 @@ tests = { }, 'open_close': {}, 'open_flags': {}, + 'pf_rollback': {}, + 'pf_tamper': {}, 'read_write': {}, 'read_write_mmap': {}, 'seek_tell': {}, diff --git a/libos/test/fs/pf_rollback.c b/libos/test/fs/pf_rollback.c new file mode 100644 index 0000000000..caffdd1616 --- /dev/null +++ b/libos/test/fs/pf_rollback.c @@ -0,0 +1,717 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* Copyright (C) 2024 Intel Corporation + * PaweÅ‚ Marczewski + * Michael Steiner + */ + +/* + * Tests for rollback protection of protected (encrypted) files + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" + +/* TODO (MST): this is "borrowed" from common/include/api.h. replace below with `#include "api.h"` + * once i figured out how to fix the meson.build files .... */ +#define __UNUSED(x) \ + do { \ + (void)(x); \ + } while (0) + +/* function to print out test result to be parsed by driver script in python. + * Note: OK in tests below should cover the case for protection-mode=strict, so for other modes some + * tests can be expected to FAIL (documented by a corresponding comment behind the call to + * test_report()*/ +#define test_report(result) printf("%s: %s\n", result, __func__) + +const int MAX_FILE_NAME_SIZE = 256; + +static const char message[] = + "short message\n"; /* a message we assume can be written in a single write */ +static const size_t message_len = sizeof(message) - 1; + +/* dummy functions which are gdb break-point targets */ +#pragma GCC push_options +#pragma GCC optimize("O0") +static void adversary_save_file(const char* path) { + __UNUSED(path); /* neeed in gdb though! */ +} +static void adversary_reset_file(const char* path) { + __UNUSED(path); /* neeed in gdb though! */ +} +static void adversary_reset_file_as(const char* path, const char* path2) { + __UNUSED(path); /* neeed in gdb though! */ + __UNUSED(path2); /* neeed in gdb though! */ +} +static void adversary_delete_file(const char* path) { + __UNUSED(path); /* neeed in gdb though! */ + /* NOTE: as of 2024-06-14 this attack will never work as the dcache never + * evicts entries and so libos thinks it still exists yet when it tries to open it (in + * encrypted_file_internal_open), pal doesn't find it which results in a PalStreamOpen failed: + * Stream does not exist (PAL_ERROR_STREAMNOTEXIST) in PalStreamOpen. + * + * This means that all of the test_delete_rollback will also fail for protection-mode=none. + * The tests though are still retained just in case dcache flushs would be eventually added. */ +} +#pragma GCC pop_options + +/* + * Non-adverserial tests + * --------------------- */ + +static void test_open_pre_existing(const char* path1) { + int fd = open(path1, O_RDWR); + if (fd < 0) { + test_report("OK"); /* Note: open only should fail in strict protection mode! */ + } else { + test_report("FAIL"); + } +} + +static void test_reopen_base(const char* path1) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); +} + +static void test_reopen_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_reopen_base(path1); + + int fd = open(path1, O_RDWR); + if (fd > 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_reopen_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_reopen_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd > 0) { + test_report("FAIL"); + } else { + test_report("OK"); + } +} + +static void test_reopen_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_reopen_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT, 0600); + if (fd > 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_reopen_renamed_base(const char* path1, const char* path2) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); + + if (rename(path1, path2) != 0) + err(1, "rename"); +} + +static void test_reopen_renamed_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_reopen_renamed_base(path1, path2); + + int fd = open(path2, O_RDWR); + if (fd > 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_reopen_renamed_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_reopen_renamed_base(path1, path2); + + int fd = open(path2, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd > 0) { + test_report("FAIL"); + } else { + test_report("OK"); + } +} + +static void test_reopen_renamed_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_reopen_renamed_base(path1, path2); + + int fd = open(path2, O_RDWR | O_CREAT, 0600); + if (fd > 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +/* + * Adverserial tests + * --------------------- */ + +static void test_rollback_after_close_base(const char* path1) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); + + adversary_save_file(path1); + + fd = open(path1, O_RDWR); + if (fd < 0) { + err(1, "re-open %s", path1); + } + + n = write(fd, message, message_len); + if (n < 0) + err(1, "posix_fd_write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "re-close %s", path1); + + adversary_reset_file(path1); +} + +static void test_rollback_after_close_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_rollback_after_close_base(path1); + + int fd = open(path1, O_RDWR); + if (fd < 0) { + test_report("OK"); /* Note: open should work in protection mode none! */ + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_close_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_rollback_after_close_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_close_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_rollback_after_close_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT, 0600); + if (fd < 0) { + test_report("OK"); /* Note: open should work in protection mode none! */ + } else { + test_report("FAIL"); + } +} + +static void test_delete_rollback_after_close_base(const char* path1) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); + + adversary_delete_file(path1); +} + +static void test_delete_rollback_after_close_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_delete_rollback_after_close_base(path1); + + int fd = open(path1, O_RDWR); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_delete_rollback_after_close_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_delete_rollback_after_close_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_delete_rollback_after_close_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_delete_rollback_after_close_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rollback_while_open(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + adversary_save_file(path1); + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + off_t ret = lseek(fd, 0, SEEK_SET); + if (ret < 0) + err(1, "seek %s", path1); + char buf[message_len]; + n = read(fd, buf, message_len); + if (n < 0) + err(1, "read %s", path1); + if ((size_t)n != message_len) + errx(1, "read less bytes than expected from %s", path1); + if (strncmp(buf, message, message_len) != 0) + errx(1, "read different bytes than expected from %s", path1); + + adversary_reset_file(path1); + + /* TODO (MST): maybe flush state here?! */ + + ret = lseek(fd, 0, SEEK_SET); + if (ret < 0) + err(1, "seek %s", path1); + n = read(fd, buf, message_len); + if (n < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_rename_base(const char* path1, const char* path2) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); + + adversary_save_file(path1); + + if (rename(path1, path2) != 0) + err(1, "rename"); + + adversary_reset_file(path1); +} + +static void test_rollback_after_rename_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_rollback_after_rename_base(path1, path2); + + int fd = open(path1, O_RDWR); + if (fd < 0) { + test_report("OK"); /* Note: open should work in protection mode none! */ + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_rename_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_rollback_after_rename_base(path1, path2); + + int fd = open(path1, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_rename_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_rollback_after_rename_base(path1, path2); + + int fd = open(path1, O_RDWR | O_CREAT, 0600); + if (fd < 0) { + test_report("OK"); /* Note: open should work in protection mode none! */ + } else { + test_report("FAIL"); + } +} + +static void test_rename_rollback_after_rename_base(const char* path1, const char* path2) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); + + adversary_save_file(path1); + + if (rename(path1, path2) != 0) + err(1, "rename"); + + adversary_reset_file_as(path1, path2); +} + +static void test_rename_rollback_after_rename_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_rename_rollback_after_rename_base(path1, path2); + + int fd = open(path2, O_RDWR); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rename_rollback_after_rename_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_rename_rollback_after_rename_base(path1, path2); + + int fd = open(path2, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rename_rollback_after_rename_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_rename_rollback_after_rename_base(path1, path2); + + int fd = open(path2, O_RDWR | O_CREAT, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_delete_rollback_after_rename_base(const char* path1, const char* path2) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); + + if (rename(path1, path2) != 0) + err(1, "rename"); + + adversary_delete_file(path2); +} + +static void test_delete_rollback_after_rename_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_delete_rollback_after_rename_base(path1, path2); + + int fd = open(path2, O_RDWR); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_delete_rollback_after_rename_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_delete_rollback_after_rename_base(path1, path2); + + int fd = open(path2, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_delete_rollback_after_rename_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + char path2[MAX_FILE_NAME_SIZE]; + snprintf(path2, MAX_FILE_NAME_SIZE, "%s/%s.renamed", work_dir, __func__); + + test_delete_rollback_after_rename_base(path1, path2); + + int fd = open(path2, O_RDWR | O_CREAT, 0600); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_unlink_base(const char* path1) { + int fd = open(path1, O_RDWR | O_EXCL | O_CREAT, 0600); + if (fd < 0) { + err(1, "open %s", path1); + } + + ssize_t n = write(fd, message, message_len); + if (n < 0) + err(1, "write %s", path1); + if ((size_t)n != message_len) + errx(1, "written less bytes than expected into %s", path1); + + if (close(fd) != 0) + err(1, "close %s", path1); + + adversary_save_file(path1); + + if (unlink(path1) != 0) + err(1, "unlink"); + + adversary_reset_file(path1); +} + +static void test_rollback_after_unlink_rw(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_rollback_after_unlink_base(path1); + + int fd = open(path1, O_RDWR); + if (fd < 0) { + test_report("OK"); + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_unlink_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_rollback_after_unlink_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + test_report("OK"); /* Note: open should work in protection mode none! */ + } else { + test_report("FAIL"); + } +} + +static void test_rollback_after_unlink_non_exclusive(const char* work_dir) { + char path1[MAX_FILE_NAME_SIZE]; + snprintf(path1, MAX_FILE_NAME_SIZE, "%s/%s", work_dir, __func__); + + test_rollback_after_unlink_base(path1); + + int fd = open(path1, O_RDWR | O_CREAT, 0600); + if (fd < 0) { + test_report("OK"); /* Note: open should work in protection mode none! */ + } else { + test_report("FAIL"); + } +} + +/* + * Overall test driver + * --------------------- */ + +static void run_tests(const char* work_dir, const char* input_file) { + /* non-adverserial ones */ + test_open_pre_existing(input_file); + test_reopen_rw(work_dir); + test_reopen_exclusive(work_dir); + test_reopen_non_exclusive(work_dir); + test_reopen_renamed_rw(work_dir); + test_reopen_renamed_exclusive(work_dir); + test_reopen_renamed_non_exclusive(work_dir); + + /* adverserial ones */ + test_rollback_after_close_rw(work_dir); + test_rollback_after_close_exclusive(work_dir); + test_rollback_after_close_non_exclusive(work_dir); + test_delete_rollback_after_close_rw(work_dir); + test_delete_rollback_after_close_exclusive(work_dir); + test_delete_rollback_after_close_non_exclusive(work_dir); + test_rollback_while_open(work_dir); + test_rollback_after_rename_rw(work_dir); + test_rollback_after_rename_exclusive(work_dir); + test_rollback_after_rename_non_exclusive(work_dir); + test_rename_rollback_after_rename_rw(work_dir); + test_rename_rollback_after_rename_exclusive(work_dir); + test_rename_rollback_after_rename_non_exclusive(work_dir); + test_delete_rollback_after_rename_rw(work_dir); + test_delete_rollback_after_rename_exclusive(work_dir); + test_delete_rollback_after_rename_non_exclusive(work_dir); + test_rollback_after_unlink_rw(work_dir); + test_rollback_after_unlink_exclusive(work_dir); + test_rollback_after_unlink_non_exclusive(work_dir); +} + +int main(int argc, char* argv[]) { + setbuf(stdout, NULL); + setbuf(stderr, NULL); + + if (argc != 3) + errx(1, "Usage: %s (with input_file assumed to be pre-created)", + argv[0]); + + const char* work_dir = argv[1]; + const char* input_file = argv[2]; + + /* all tests started in process leader */ + run_tests(work_dir, input_file); + + printf("TEST OK\n"); + exit(0); +} diff --git a/libos/test/fs/pf_rollback.gdb b/libos/test/fs/pf_rollback.gdb new file mode 100644 index 0000000000..6fd3d0d4e3 --- /dev/null +++ b/libos/test/fs/pf_rollback.gdb @@ -0,0 +1,73 @@ +set breakpoint pending on +set pagination off +set backtrace past-main on + +# We want to check what happens in the child process after fork() +set follow-fork-mode child + +# Cannot detach after fork because of some bug in SGX version of GDB (GDB would segfault) +set detach-on-fork off + + + +break adversary_save_file +commands +python +from shutil import copyfile +from gdb_helper import adversary_do +adversary_do(lambda external_path, external_path_saved, external_path2: copyfile(external_path, external_path_saved)) +end + +continue +end + + +break adversary_reset_file +commands +python +from shutil import move +from gdb_helper import adversary_do +adversary_do(lambda external_path, external_path_saved, external_path2: move(external_path_saved, external_path)) +end + +continue +end + + +break adversary_reset_file_as +commands +python +from shutil import move +from gdb_helper import adversary_do +adversary_do(lambda external_path, external_path_saved, external_path2: move(external_path_saved, external_path2)) +end + +continue +end + + +break adversary_delete_file +commands +python +from pathlib import Path +from gdb_helper import adversary_do +adversary_do(lambda external_path, external_path_saved, external_path2: Path(external_path).unlink()) +end + +continue +end + + +break die_or_inf_loop +commands + echo EXITING GDB WITH A GRAMINE ERROR\n + quit +end + +break exit +commands + echo EXITING GDB WITHOUT A GRAMINE ERROR\n + quit +end + +run diff --git a/libos/test/fs/pf_rollback.manifest.template b/libos/test/fs/pf_rollback.manifest.template new file mode 100644 index 0000000000..93d48d1d24 --- /dev/null +++ b/libos/test/fs/pf_rollback.manifest.template @@ -0,0 +1,30 @@ +loader.entrypoint = "file:{{ gramine.libos }}" +loader.log_level ="trace" # DEBUG +libos.entrypoint = "{{ entrypoint }}" + +loader.env.LD_LIBRARY_PATH = "/lib:{{ arch_libdir }}:/usr/{{ arch_libdir }}" +loader.insecure__use_cmdline_argv = true + +fs.mounts = [ + { path = "/lib", uri = "file:{{ gramine.runtimedir(libc) }}" }, + { path = "/{{ entrypoint }}", uri = "file:{{ binary_dir }}/{{ entrypoint }}" }, + { path = "/bin", uri = "file:/bin" }, + + { type = "encrypted", protection_mode = "strict", path = "/tmp/enc_input/pm_strict", uri = "file:tmp/enc_input/pm_strict" }, + { type = "encrypted", protection_mode = "non-strict", path = "/tmp/enc_input/pm_non-strict", uri = "file:tmp/enc_input/pm_non-strict" }, + { type = "encrypted", protection_mode = "none", path = "/tmp/enc_input/pm_none", uri = "file:tmp/enc_input/pm_none" }, +] + +sgx.max_threads = {{ '1' if env.get('EDMM', '0') == '1' else '16' }} +sgx.debug = true +sgx.edmm_enable = {{ 'true' if env.get('EDMM', '0') == '1' else 'false' }} + + +sgx.trusted_files = [ + "file:{{ gramine.libos }}", + "file:{{ gramine.runtimedir(libc) }}/", + "file:{{ binary_dir }}/{{ entrypoint }}", +] + +# See the `keys.c` test. +fs.insecure__keys.default = "ffeeddccbbaa99887766554433221100" diff --git a/libos/test/fs/pf_tamper.c b/libos/test/fs/pf_tamper.c new file mode 100644 index 0000000000..1e262874f9 --- /dev/null +++ b/libos/test/fs/pf_tamper.c @@ -0,0 +1,35 @@ +#include "common.h" + +static void read_complete_file(const char* path) { + int fd = open(path, O_RDONLY); + if (fd < 0) { + printf("ERROR: Failed to open input file %s: %s\n", path, strerror(errno)); + return; + } + + char buf[1024]; + while (true) { + ssize_t ret = read(fd, buf, sizeof(buf)); + if (ret < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + printf("ERROR: Failed to read file %s: %s\n", path, strerror(errno)); + return; + } + if (ret == 0) + break; + } + + if (close(fd) != 0) + printf("ERROR: Failed to close file %s: %s\n", path, strerror(errno)); +} + + +int main(int argc, char* argv[]) { + if (argc < 2) + fatal_error("Usage: %s \n", argv[0]); + + setup(); + read_complete_file(argv[1]); + return 0; +} diff --git a/libos/test/fs/pf_tamper.manifest.template b/libos/test/fs/pf_tamper.manifest.template new file mode 100644 index 0000000000..7028233a0c --- /dev/null +++ b/libos/test/fs/pf_tamper.manifest.template @@ -0,0 +1,28 @@ +loader.entrypoint = "file:{{ gramine.libos }}" +loader.log_level ="trace" # DEBUG +libos.entrypoint = "{{ entrypoint }}" + +loader.env.LD_LIBRARY_PATH = "/lib:{{ arch_libdir }}:/usr/{{ arch_libdir }}" +loader.insecure__use_cmdline_argv = true + +fs.mounts = [ + { path = "/lib", uri = "file:{{ gramine.runtimedir(libc) }}" }, + { path = "/{{ entrypoint }}", uri = "file:{{ binary_dir }}/{{ entrypoint }}" }, + { path = "/bin", uri = "file:/bin" }, + + { type = "encrypted", protection_mode = "non-strict", path = "/tmp/enc_output", uri = "file:tmp/enc_output" }, +] + +sgx.max_threads = {{ '1' if env.get('EDMM', '0') == '1' else '16' }} +sgx.debug = true +sgx.edmm_enable = {{ 'true' if env.get('EDMM', '0') == '1' else 'false' }} + + +sgx.trusted_files = [ + "file:{{ gramine.libos }}", + "file:{{ gramine.runtimedir(libc) }}/", + "file:{{ binary_dir }}/{{ entrypoint }}", +] + +# See the `keys.c` test. +fs.insecure__keys.default = "ffeeddccbbaa99887766554433221100" diff --git a/libos/test/fs/test_enc.py b/libos/test/fs/test_enc.py index 3b83a33879..9f75149fdf 100644 --- a/libos/test/fs/test_enc.py +++ b/libos/test/fs/test_enc.py @@ -179,17 +179,20 @@ def test_500_invalid(self): # prepare valid encrypted file (largest one for maximum possible corruptions) original_input = self.OUTPUT_FILES[-1] self.__encrypt_file(self.INPUT_FILES[-1], original_input) + shutil.copy(original_input, original_input+".save") # Save for debugging as we overwrite original below # generate invalid files based on the above self.__corrupt_file(original_input, invalid_dir) # try to decrypt invalid files + failed = False for name in os.listdir(invalid_dir): invalid = os.path.join(invalid_dir, name) output_path = os.path.join(self.OUTPUT_DIR, name) - input_path = os.path.join(invalid_dir, os.path.basename(original_input)) # copy the file so it has the original file name (for allowed path check) + input_path = original_input shutil.copy(invalid, input_path) + # test decryption using the pf-crypt tool try: args = ['decrypt', '-V', '-w', self.WRAP_KEY, '-i', input_path, '-o', output_path] self.__pf_crypt(args) @@ -197,5 +200,73 @@ def test_500_invalid(self): # decryption of invalid file must fail with -1 (wrapped to 255) self.assertEqual(exc.returncode, 255) else: - print('[!] Fail: successfully decrypted file: ' + name) - self.fail() + print('[!] Fail: successfully decrypted file with cipher utility: ' + name) + failed = True + + # test decryption as part of reading file in program running with gramine + stdout, stderr = self.run_binary(['pf_tamper', input_path]) + try: + self.assertIn('ERROR: ', stdout) + # TODO: check also that we updated map in trace/stderr? + # DEBUG: self.assertIn('truncate(' + path_1 + ') to ' + str(size_out) + ' OK', stdout) + except: + print('[!] Fail: successfully decrypted file with gramine: ' + name) + failed = True + + if failed: + self.fail() + + # checks rollback protection + def test_600_gdb_pf_rollback(self): + # To run this test manually, encrypt a (contained in ) with the + # default key from manifest and use: + # GDB=1 GDB_TTY=1 GDB_SCRIPT=pf_rollback.gdb gramine-[sgx|direct] pf_rollback + # + expected_results = { # with entres for protection-modes strict, non-strict & none + 'test_open_pre_existing': [ 'OK', 'FAIL', 'FAIL' ], + 'test_reopen_rw': [ 'OK', 'OK', 'OK' ], + 'test_reopen_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_reopen_non_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_reopen_renamed_rw': [ 'OK', 'OK', 'OK' ], + 'test_reopen_renamed_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_reopen_renamed_non_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_rollback_after_close_rw': [ 'OK', 'OK', 'FAIL' ], + 'test_rollback_after_close_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_rollback_after_close_non_exclusive': [ 'OK', 'OK', 'FAIL' ], + 'test_delete_rollback_after_close_rw': [ 'OK', 'OK', 'OK' ], + 'test_delete_rollback_after_close_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_delete_rollback_after_close_non_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_rollback_while_open': [ 'OK', 'OK', 'OK' ], + 'test_rollback_after_rename_rw': [ 'OK', 'OK', 'FAIL' ], + 'test_rollback_after_rename_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_rollback_after_rename_non_exclusive': [ 'OK', 'OK', 'FAIL' ], + 'test_rename_rollback_after_rename_rw': [ 'OK', 'OK', 'OK' ], + 'test_rename_rollback_after_rename_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_rename_rollback_after_rename_non_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_delete_rollback_after_rename_rw': [ 'OK', 'OK', 'OK' ], + 'test_delete_rollback_after_rename_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_delete_rollback_after_rename_non_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_rollback_after_unlink_rw': [ 'OK', 'OK', 'FAIL' ], + 'test_rollback_after_unlink_exclusive': [ 'OK', 'OK', 'OK' ], + 'test_rollback_after_unlink_non_exclusive': [ 'OK', 'OK', 'FAIL' ], + } + for mode_str, mode_idx in { 'strict': 0, 'non-strict': 1, 'none': 2}.items(): + work_dir = self.ENCRYPTED_DIR + f"/pm_{mode_str}" + internal_work_dir = '/'+work_dir + os.mkdir(work_dir) + input_file = work_dir + "/input_file" + self.__encrypt_file(self.INPUT_FILES[-1], input_file) + internal_input_file = '/'+input_file + stdout, _ = self.run_gdb(['pf_rollback', internal_work_dir, internal_input_file], 'pf_rollback.gdb', hide_tty=False) + + # expected test results + for key, val in expected_results.items(): + self.assertIn(f'{val[mode_idx]}: {key}', stdout) + # Note: for rollback attacks there are also log-messages of adverserial actions along the lines of ... + # self.assertIn('OK: test_rollback_after_close_rw in adversary_save_file({internal_work_dir}/test_rollback_after_close_rw)', stdout) + # self.assertIn('OK: test_rollback_after_close_rw in adversary_reset_file({internal_work_dir}/test_rollback_after_close_rw)', stdout) + # ... and delete_rollback attacks provide + # self.assertIn(f'OK: test_delete_rollback_after_rename_rw in adversary_delete_file({internal_work_dir}/test_delete_rollback_after_rename_rw.renamed)', stdout) + self.assertIn('TEST OK', stdout) + self.assertIn('EXITING GDB WITHOUT A GRAMINE ERROR', stdout) + self.assertNotIn('EXITING GDB WITH A GRAMINE ERROR', stdout) diff --git a/libos/test/fs/test_fs.py b/libos/test/fs/test_fs.py index 2fb768d252..4345d191c0 100644 --- a/libos/test/fs/test_fs.py +++ b/libos/test/fs/test_fs.py @@ -2,6 +2,7 @@ import os import shutil import unittest +import conftest from graminelibos.regression import ( HAS_SGX, @@ -31,7 +32,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - shutil.rmtree(cls.TEST_DIR) + if not conftest.option.skip_teardown: + shutil.rmtree(cls.TEST_DIR) def setUp(self): # clean output for each test @@ -336,7 +338,8 @@ def setUp(self): os.mkdir(self.TEST_DIR) def tearDown(self): - shutil.rmtree(self.TEST_DIR) + if not conftest.option.skip_teardown: + shutil.rmtree(self.TEST_DIR) def _test_multiple_writers(self, n_lines, n_processes, n_threads): output_path = os.path.join(self.TEST_DIR, 'output.txt') diff --git a/libos/test/fs/tests.toml b/libos/test/fs/tests.toml index 362b737322..811e57203f 100644 --- a/libos/test/fs/tests.toml +++ b/libos/test/fs/tests.toml @@ -13,6 +13,8 @@ manifests = [ "multiple_writers", "open_close", "open_flags", + "pf_rollback", + "pf_tamper", "read_write", "read_write_mmap", "seek_tell", diff --git a/libos/test/regression/fcntl_lock_child_only.manifest.template b/libos/test/regression/fcntl_lock_child_only.manifest.template index 202bfe59ed..10f95dc2d7 100644 --- a/libos/test/regression/fcntl_lock_child_only.manifest.template +++ b/libos/test/regression/fcntl_lock_child_only.manifest.template @@ -6,7 +6,8 @@ fs.mounts = [ { path = "/lib", uri = "file:{{ gramine.runtimedir(libc) }}" }, { path = "/{{ entrypoint }}", uri = "file:{{ binary_dir }}/{{ entrypoint }}" }, - { type = "encrypted", path = "/tmp_enc/", uri = "file:tmp_enc/" }, + { type = "encrypted", protection_mode = "non-strict", path = "/tmp_enc/", uri = "file:tmp_enc/" }, + {# this test work only in non-strict mode until we have full multi-process support for rollback protected files .. #} ] fs.insecure__keys.default = "ffeeddccbbaa99887766554433221100" diff --git a/libos/test/regression/manifest.template b/libos/test/regression/manifest.template index ab236b1cf2..ec07e32cc1 100644 --- a/libos/test/regression/manifest.template +++ b/libos/test/regression/manifest.template @@ -15,7 +15,12 @@ fs.mounts = [ { path = "/bin", uri = "file:/bin" }, { type = "tmpfs", path = "/mnt/tmpfs" }, - { type = "encrypted", path = "/tmp_enc", uri = "file:tmp_enc", key_name = "my_custom_key" }, +{% if entrypoint in [ "send_handle" ] %} + {# these tests work only in non-strict mode until we have full multi-process support for rollback protected files .. #} + { type = "encrypted", protection_mode = "non-strict", path = "/tmp_enc", uri = "file:tmp_enc", key_name = "my_custom_key" }, +{% else %} + { type = "encrypted", protection_mode = "strict", path = "/tmp_enc", uri = "file:tmp_enc", key_name = "my_custom_key" }, +{% endif %} { type = "encrypted", path = "/tmp_enc/mrenclaves", uri = "file:tmp_enc/mrenclaves", key_name = "_sgx_mrenclave" }, { type = "encrypted", path = "/tmp_enc/mrsigners", uri = "file:tmp_enc/mrsigners", key_name = "_sgx_mrsigner" }, ] diff --git a/libos/test/regression/meson.build b/libos/test/regression/meson.build index cbd141d51f..37a4928aae 100644 --- a/libos/test/regression/meson.build +++ b/libos/test/regression/meson.build @@ -102,6 +102,7 @@ tests = { 'readdir': {}, 'rename_unlink': {}, 'rename_unlink_fchown': {}, + 'rollback': {}, 'run_test': { 'include_directories': include_directories( # for `gramine_entry_api.h` diff --git a/libos/test/regression/rollback.c b/libos/test/regression/rollback.c new file mode 100644 index 0000000000..c862c91917 --- /dev/null +++ b/libos/test/regression/rollback.c @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* Copyright (C) 2024 Intel Corporation + * Michael Steiner + */ + +/* Test for setting and reading encrypted files keys (/dev/attestation/keys). */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rw_file.h" + +// TODO (MST): implement me +// - tests +// - handles correctly paths which are not encryped files +// - reports correctly presence/absence of files +// - for existing files, reports correct state (one for each state) + +int main(int argc, char** argv) { + return 0; +} diff --git a/libos/test/regression/tests.toml b/libos/test/regression/tests.toml index 16033ebc79..a92fc69a89 100644 --- a/libos/test/regression/tests.toml +++ b/libos/test/regression/tests.toml @@ -101,6 +101,7 @@ manifests = [ "readdir", "rename_unlink", "rename_unlink_fchown", + "rollback", "run_test", "rwlock", "sched", diff --git a/libos/test/regression/tests_musl.toml b/libos/test/regression/tests_musl.toml index 2c5de8d5ee..e47f892d80 100644 --- a/libos/test/regression/tests_musl.toml +++ b/libos/test/regression/tests_musl.toml @@ -103,6 +103,7 @@ manifests = [ "readdir", "rename_unlink", "rename_unlink_fchown", + "rollback", "run_test", "rwlock", "sched", diff --git a/python/graminelibos/manifest_check.py b/python/graminelibos/manifest_check.py index 94d19a316a..e1b965498d 100644 --- a/python/graminelibos/manifest_check.py +++ b/python/graminelibos/manifest_check.py @@ -30,6 +30,7 @@ Required('type'): 'encrypted', Required('uri'): _uri, 'key_name': str, + 'protection_mode': str, }, { Required('type'): 'tmpfs', diff --git a/python/graminelibos/regression.py b/python/graminelibos/regression.py index 5a966dbd6b..8e6410adff 100644 --- a/python/graminelibos/regression.py +++ b/python/graminelibos/regression.py @@ -192,7 +192,7 @@ def has_debug(self): dump = p.stdout.decode() return '.debug_info' in dump - def run_gdb(self, args, gdb_script, **kwds): + def run_gdb(self, args, gdb_script, hide_tty=True, **kwds): prefix = ['gdb', '-q'] env = os.environ.copy() if HAS_SGX: @@ -202,8 +202,10 @@ def run_gdb(self, args, gdb_script, **kwds): else: prefix += ['-x', fspath(self.pal_path / 'gdb_integration/gramine_linux_gdb.py')] - # Override TTY, as apparently os.setpgrp() confuses GDB and causes it to hang. - prefix += ['-x', gdb_script, '-batch', '-tty=/dev/null'] + prefix += ['-x', gdb_script, '-batch'] + if hide_tty: + # Override TTY, as apparently os.setpgrp() confuses GDB and causes it to hang. + prefix += ['-tty=/dev/null'] prefix += ['--args'] return self.run_binary(args, prefix=prefix, env=env, **kwds) diff --git a/tools/gramine.in b/tools/gramine.in index 249581a9d6..9b76565730 100755 --- a/tools/gramine.in +++ b/tools/gramine.in @@ -27,8 +27,12 @@ if [ "$GDB" == "1" ]; then PREFIX+=("-x" "$HOST_PAL_PATH/gdb_integration/gramine_linux_gdb.py") fi if [ "$GDB_SCRIPT" != "" ]; then - # Run a script in batch mode, and without TTY (so that it can be piped, redirected etc.) - PREFIX+=("-x" "$GDB_SCRIPT" "-batch" "-tty=/dev/null") + # Run a script in batch mode, and ... + PREFIX+=("-x" "$GDB_SCRIPT") + # ... optionally without TTY (so that it can be piped, redirected etc.) + if [ -z ${GDB_TTY+x} ]; then + PREFIX+=("-batch" "-tty=/dev/null") + fi fi PREFIX+=("--args") fi diff --git a/tools/sgx/common/pf_util.c b/tools/sgx/common/pf_util.c index eadd0323c3..1a1fe7b8d8 100644 --- a/tools/sgx/common/pf_util.c +++ b/tools/sgx/common/pf_util.c @@ -329,8 +329,8 @@ int pf_encrypt_file(const char* input_path, const char* output_path, const pf_ke norm_output_path); pf_handle_t handle = (pf_handle_t)&output; - pf_status_t pfs = pf_open(handle, norm_output_path, /*size=*/0, PF_FILE_MODE_WRITE, - /*create=*/true, wrap_key, &pf); + pf_status_t pfs = pf_open(handle, norm_output_path, /*size=*/0, PF_FILE_MODE_WRITE, + /*create=*/true, wrap_key, NULL, &pf); if (PF_FAILURE(pfs)) { ERROR("Failed to open output PF: %s\n", pf_strerror(pfs)); goto out; @@ -371,7 +371,7 @@ int pf_encrypt_file(const char* input_path, const char* output_path, const pf_ke out: if (pf) { - if (PF_FAILURE(pf_close(pf))) { + if (PF_FAILURE(pf_close(pf, NULL))) { ERROR("failed to close PF\n"); ret = -1; } @@ -438,7 +438,7 @@ int pf_decrypt_file(const char* input_path, const char* output_path, bool verify } pf_status_t pfs = pf_open((pf_handle_t)&input, norm_input_path, input_size, PF_FILE_MODE_READ, - /*create=*/false, wrap_key, &pf); + /*create=*/false, wrap_key, NULL, &pf); if (PF_FAILURE(pfs)) { ERROR("Opening protected input file failed: %s\n", pf_strerror(pfs)); goto out; @@ -495,7 +495,7 @@ int pf_decrypt_file(const char* input_path, const char* output_path, bool verify free(norm_input_path); free(chunk); if (pf) - pf_close(pf); + pf_close(pf, NULL); if (input >= 0) close(input); if (output >= 0) diff --git a/tools/sgx/pf_tamper/pf_tamper.c b/tools/sgx/pf_tamper/pf_tamper.c index 004663f0e1..50f40d2e72 100644 --- a/tools/sgx/pf_tamper/pf_tamper.c +++ b/tools/sgx/pf_tamper/pf_tamper.c @@ -212,10 +212,10 @@ static void tamper_truncate(void) { truncate_file("trunc_data_3", 3 * PF_NODE_SIZE + PF_NODE_SIZE / 2); /* extend */ + /* Note: as mentioned below in tamper_modify() for meta_dec.file_size, we mostly allow the + * actual file to be longer than what the header size, the only thing we require is that the + * file size is a multiple of PF_NODE_SIZE, so just verify this */ truncate_file("extend_0", g_input_size + 1); - truncate_file("extend_1", g_input_size + PF_NODE_SIZE / 2); - truncate_file("extend_2", g_input_size + PF_NODE_SIZE); - truncate_file("extend_3", g_input_size + PF_NODE_SIZE + PF_NODE_SIZE / 2); } /* returns mmap'd output contents */ @@ -271,23 +271,36 @@ static void pf_encrypt(const void* decrypted, size_t size, const pf_key_t* key, } while (0) /* if update is true, also create a file with correct metadata MAC */ -#define BREAK_PF(suffix, update, ...) do { \ - __BREAK_PF(suffix, __VA_ARGS__); \ - if (update) { \ - __BREAK_PF(suffix "_fixed", __VA_ARGS__ { \ - pf_encrypt(meta_dec, sizeof(*meta_dec), &g_meta_key, \ - &meta->plaintext_part.metadata_mac, meta->encrypted_part, \ - "metadata"); \ - } ); \ - } \ -} while (0) - -#define BREAK_MHT(suffix, ...) do { \ - __BREAK_PF(suffix, __VA_ARGS__ { \ - pf_encrypt(mht_dec, sizeof(*mht_dec), &meta_dec->root_mht_node_key, \ - &meta_dec->root_mht_node_mac, mht_enc, "mht"); \ - } ); \ -} while (0) +#define BREAK_PLN(suffix, update, ...) \ + do { \ + __BREAK_PF(suffix, __VA_ARGS__); \ + if (update) { \ + __BREAK_PF( \ + suffix "_fixed", __VA_ARGS__ { \ + pf_encrypt(meta_dec, sizeof(*meta_dec), &g_meta_key, \ + &meta->plaintext_part.metadata_mac, meta->encrypted_part, \ + "metadata"); \ + }); \ + } \ + } while (0) + +#define BREAK_DEC(suffix, ...) \ + do { \ + __BREAK_PF( \ + suffix, __VA_ARGS__ { \ + pf_encrypt(meta_dec, sizeof(*meta_dec), &g_meta_key, \ + &meta->plaintext_part.metadata_mac, meta->encrypted_part, "metadata"); \ + }); \ + } while (0) + +#define BREAK_MHT(suffix, ...) \ + do { \ + __BREAK_PF( \ + suffix, __VA_ARGS__ { \ + pf_encrypt(mht_dec, sizeof(*mht_dec), &meta_dec->root_mht_node_key, \ + &meta_dec->root_mht_node_mac, mht_enc, "mht"); \ + }); \ + } while (0) #define LAST_BYTE(array) (((uint8_t*)&array)[sizeof(array) - 1]) @@ -303,60 +316,54 @@ static void tamper_modify(void) { FATAL("Out of memory\n"); /* plain part of the metadata isn't covered by the MAC so no point updating it */ - BREAK_PF("meta_plain_id_0", /*update=*/false, - { meta->plaintext_part.file_id = 0; }); - BREAK_PF("meta_plain_id_1", /*update=*/false, - { meta->plaintext_part.file_id = UINT64_MAX; }); - BREAK_PF("meta_plain_version_0", /*update=*/false, - { meta->plaintext_part.major_version = 0; }); - BREAK_PF("meta_plain_version_1", /*update=*/false, - { meta->plaintext_part.major_version = 0xff; }); - BREAK_PF("meta_plain_version_2", /*update=*/false, - { meta->plaintext_part.minor_version = 0xff; }); + BREAK_PLN("meta_plain_id_0", /*update=*/false, + { meta->plaintext_part.file_id = 0; }); + BREAK_PLN("meta_plain_id_1", /*update=*/false, + { meta->plaintext_part.file_id = UINT64_MAX; }); + BREAK_PLN("meta_plain_version_0", /*update=*/false, + { meta->plaintext_part.major_version = 0; }); + BREAK_PLN("meta_plain_version_1", /*update=*/false, + { meta->plaintext_part.major_version = 0xff; }); + /* Note: we only test (equality) on major version but nothing about minor_version, so no + * point in tampering with meta->plaintext_part.minor_version ... */ /* metadata_key_nonce is the keying material for encrypted metadata key derivation, so create * also PFs with updated MACs */ - BREAK_PF("meta_plain_nonce_0", /*update=*/true, - { meta->plaintext_part.metadata_key_nonce[0] ^= 1; }); - BREAK_PF("meta_plain_nonce_1", /*update=*/true, - { LAST_BYTE(meta->plaintext_part.metadata_key_nonce) ^= 0xfe; }); - BREAK_PF("meta_plain_mac_0", /*update=*/true, - { meta->plaintext_part.metadata_mac[0] ^= 0xfe; }); - BREAK_PF("meta_plain_mac_1", /*update=*/true, - { LAST_BYTE(meta->plaintext_part.metadata_mac) &= 1; }); - - BREAK_PF("meta_enc_filename_0", /*update=*/true, - { meta_dec->file_path[0] = 0; }); - BREAK_PF("meta_enc_filename_1", /*update=*/true, - { meta_dec->file_path[0] ^= 1; }); - BREAK_PF("meta_enc_filename_2", /*update=*/true, - { LAST_BYTE(meta_dec->file_path) ^= 0xfe; }); - BREAK_PF("meta_enc_size_0", /*update=*/true, - { meta_dec->file_size = 0; }); - BREAK_PF("meta_enc_size_1", /*update=*/true, - { meta_dec->file_size = g_input_size - 1; }); - BREAK_PF("meta_enc_size_2", /*update=*/true, - { meta_dec->file_size = g_input_size + 1; }); - BREAK_PF("meta_enc_size_3", /*update=*/true, - { meta_dec->file_size = UINT64_MAX; }); - BREAK_PF("meta_enc_mht_key_0", /*update=*/true, - { meta_dec->root_mht_node_key[0] ^= 1; }); - BREAK_PF("meta_enc_mht_key_1", /*update=*/true, - { LAST_BYTE(meta_dec->root_mht_node_key) ^= 0xfe; }); - BREAK_PF("meta_enc_mht_mac_0", /*update=*/true, - { meta_dec->root_mht_node_mac[0] ^= 1; }); - BREAK_PF("meta_enc_mht_mac_1", /*update=*/true, - { LAST_BYTE(meta_dec->root_mht_node_mac) ^= 0xfe; }); - BREAK_PF("meta_enc_data_0", /*update=*/true, - { meta_dec->file_data[0] ^= 0xfe; }); - BREAK_PF("meta_enc_data_1", /*update=*/true, - { LAST_BYTE(meta_dec->file_data) ^= 1; }); - - /* padding is ignored */ - BREAK_PF("meta_padding_0", /*update=*/false, - { meta->padding[0] ^= 1; }); - BREAK_PF("meta_padding_1", /*update=*/false, - { LAST_BYTE(meta->padding) ^= 0xfe; }); + BREAK_PLN("meta_plain_nonce_0", /*update=*/true, + { meta->plaintext_part.metadata_key_nonce[0] ^= 1; }); + BREAK_PLN("meta_plain_nonce_1", /*update=*/true, + { LAST_BYTE(meta->plaintext_part.metadata_key_nonce) ^= 0xfe; }); + BREAK_PLN("meta_plain_mac_0", /*update=*/false, // update would overwrite the tampering + { meta->plaintext_part.metadata_mac[0] ^= 0xfe; }); + BREAK_PLN("meta_plain_mac_1", /*update=*/false, // update would overwrite the tampering + { LAST_BYTE(meta->plaintext_part.metadata_mac) ^= 1; }); + BREAK_PLN("meta_plain_encrypted_0", /*update=*/false, // update would overwrite the tampering + { meta->encrypted_part[0] ^= 1; }); + BREAK_PLN("meta_plain_encrypted_1", /*update=*/false, // update would overwrite the tampering + { LAST_BYTE(meta->encrypted_part) ^= 1; }); + + BREAK_DEC("meta_enc_filename_0", { meta_dec->file_path[0] = 0; }); + BREAK_DEC("meta_enc_filename_1", { meta_dec->file_path[0] ^= 1; }); + BREAK_DEC("meta_enc_filename_2", { + meta_dec->file_path[strlen(meta_dec->file_path) - 1] = + '\0'; // shorten path by one character + }); + /* Note: we do not test generally whether file is longer than meta_dec_file indicates, in + * particular we do not test it for the case where the header says it is empty. So test only + * size modification which interfer with the mht tree but not with meta_dec->file_size = 0. */ + BREAK_DEC("meta_enc_size_0", { meta_dec->file_size = g_input_size - PF_NODE_SIZE; }); + BREAK_DEC("meta_enc_size_1", { meta_dec->file_size = g_input_size - 1; }); + BREAK_DEC("meta_enc_size_2", { meta_dec->file_size = g_input_size + PF_NODE_SIZE; }); + BREAK_DEC("meta_enc_size_3", { meta_dec->file_size = UINT64_MAX; }); + BREAK_DEC("meta_enc_size_4", { meta_dec->file_size = g_input_size + 1; }); + BREAK_DEC("meta_enc_mht_key_0", { meta_dec->root_mht_node_key[0] ^= 1; }); + BREAK_DEC("meta_enc_mht_key_1", { LAST_BYTE(meta_dec->root_mht_node_key) ^= 0xfe; }); + BREAK_DEC("meta_enc_mht_mac_0", { meta_dec->root_mht_node_mac[0] ^= 1; }); + BREAK_DEC("meta_enc_mht_mac_1", { LAST_BYTE(meta_dec->root_mht_node_mac) ^= 0xfe; }); + /* Note: no poing in tampering with (decrypted) meta_dec->file_data as there is no way to + * detect such tampering, the re-encryption would turn it in authentic (different) data .... */ + + /* Note: padding is ignored during processing, so no point in tampering meta.padding */ BREAK_MHT("mht_0", { mht_dec->data_nodes_crypto[0].key[0] ^= 1; }); BREAK_MHT("mht_1", { mht_dec->data_nodes_crypto[0].mac[0] ^= 1; }); @@ -380,11 +387,9 @@ static void tamper_modify(void) { }); /* data nodes start from node #2 */ - BREAK_PF("data_0", /*update=*/false, - { *(out + 2 * PF_NODE_SIZE) ^= 1; }); - BREAK_PF("data_1", /*update=*/false, - { *(out + 3 * PF_NODE_SIZE - 1) ^= 1; }); - BREAK_PF("data_2", /*update=*/false, { + BREAK_PLN("data_0", /*update=*/false, { *(out + 2 * PF_NODE_SIZE) ^= 1; }); + BREAK_PLN("data_1", /*update=*/false, { *(out + 3 * PF_NODE_SIZE - 1) ^= 1; }); + BREAK_PLN("data_2", /*update=*/false, { /* swap data nodes */ memcpy(out + 2 * PF_NODE_SIZE, g_input_data + 3 * PF_NODE_SIZE, PF_NODE_SIZE); memcpy(out + 3 * PF_NODE_SIZE, g_input_data + 2 * PF_NODE_SIZE, PF_NODE_SIZE);