/* * zgetdump - Tool for copying and converting System z dumps * * Utilities to decrypt secure execution guest dumps. * * Copyright IBM Corp. 2001, 2021 * * s390-tools is free software; you can redistribute it and/or modify * it under the terms of the MIT license. See LICENSE for details. */ #include "pv_utils.h" #include #include #include #include #include #include #include #include #include #include #include "lib/zt_common.h" #include "lib/util_log.h" #include "libpv/crypto.h" #include "libpv/se-hdr.h" #include "pv_defs.h" WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(Elf64_Phdr, free) /* Definitions for decrypting memory and deriving dump key */ #define PV_CSS_PAGESIZE 0x1000U /* Configuration storage state page size */ #define PV_DUMP_V1_HKDF_INFO "IBM Z Ultravisor Dump" #define PV_DUMP_V1_HKDF_LEN 32 #define PV_DUMP_V1_HKDF_FUN EVP_sha512() #define PV_DUMP_V1_CIPHER EVP_aes_256_gcm() static gboolean u64_checked_add(u64 *res, u64 lhs, u64 rhs) { guint64 _res = 0; gboolean success = g_uint64_checked_add(&_res, (guint64)lhs, (guint64)rhs); *res = (u64)_res; return success; } static u64 page_offset(u64 addr) { return addr % PV_CSS_PAGESIZE; } static u64 page_index(u64 addr) { return addr / PV_CSS_PAGESIZE; } static u64 page_start_addr(u64 page_idx, GError **error) { gboolean success; uint64_t ret; success = g_uint64_checked_mul(&ret, page_idx, PV_CSS_PAGESIZE); if (G_UNLIKELY(!success)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PAGE_START_ADDR_OVERFLOW, _("UInt overflow detected: %s: pageidx: %#llx"), __func__, page_idx); } return ret; } static u64 page_end_addr(u64 page_idx, GError **error) { gboolean success; uint64_t ret; /* (page_idx + 1) * PV_CSS_PAGESIZE - 1; */ success = g_uint64_checked_add(&ret, page_idx, 1); success &= g_uint64_checked_mul(&ret, ret, PV_CSS_PAGESIZE); ret -= 1; if (G_UNLIKELY(!success)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PAGE_END_ADDR_OVERFLOW, _("UInt overflow detected: %s: pageidx: %#llx"), __func__, page_idx); } return ret; } static void pv_dump_completion_v1_free(pv_dump_completion_v1_t *cpl) { pv_dump_completion_free((pv_dump_completion_t *)cpl); } WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(pv_dump_completion_v1_t, pv_dump_completion_v1_free) void pv_dump_completion_free(pv_dump_completion_t *cpl) { if (!cpl) return; if (cpl->version == PV_COMPL_DATA_VERSION_1) { pv_dump_completion_v1_t *cpl_v1 = (pv_dump_completion_v1_t *)cpl; OPENSSL_cleanse(&cpl_v1->data, sizeof(cpl_v1->data)); } g_free(cpl); } GBytes *pv_derive_dump_key_v1(const pv_dump_completion_data_v1_t *cpl_data, GBytes *cck, GError **error) { g_autoptr(GBytes) salt = NULL, info = NULL; size_t cck_size; size_t exp_cck_size = sizeof_field(struct pv_hdr_encrypted, cust_comm_key); assert(cpl_data->aad.version == PV_COMPL_DATA_VERSION_1); cck_size = g_bytes_get_size(cck); if (cck_size != exp_cck_size) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_WRONG_CCK_SIZE, _("Wrong key size: expected %lu != actual %lu"), exp_cck_size, cck_size); return NULL; } salt = g_bytes_new(&cpl_data->aad.seed, sizeof(cpl_data->aad.seed)); info = g_bytes_new(PV_DUMP_V1_HKDF_INFO, strlen(PV_DUMP_V1_HKDF_INFO)); return pv_hkdf_extract_and_expand(PV_DUMP_V1_HKDF_LEN, cck, salt, info, PV_DUMP_V1_HKDF_FUN, error); } pv_dump_completion_v1_t *pv_decrypt_dump_completion_v1(const pv_dump_completion_data_v1_t *cpl_data, GBytes *dump_key, GError **error) { g_autoptr(GBytes) encr = NULL, aad = NULL, tag = NULL, decr = NULL, iv = NULL; g_autoptr(pv_dump_completion_v1_t) cpl = NULL; PvCipherParms params; size_t copied; void *tmp; assert(cpl_data->aad.version == PV_COMPL_DATA_VERSION_1); encr = g_bytes_new(&cpl_data->confidential_area, sizeof(cpl_data->confidential_area)); aad = g_bytes_new(&cpl_data->aad, sizeof(cpl_data->aad)); tag = g_bytes_new(&cpl_data->tag, sizeof(cpl_data->tag)); iv = g_bytes_new(&cpl_data->aad.iv, sizeof(cpl_data->aad.iv)); params.cipher = PV_DUMP_V1_CIPHER; params.key = dump_key; params.iv = iv; params.tag_size = g_bytes_get_size(tag); if (pv_gcm_decrypt(encr, aad, tag, ¶ms, &decr, error) < 0) return NULL; cpl = g_malloc(sizeof(*cpl)); cpl->super.version = PV_COMPL_DATA_VERSION_1; tmp = pv_gbytes_memcpy(&cpl->data.aad, sizeof(cpl->data.aad), aad, &copied); if (!tmp || copied != sizeof(cpl->data.aad)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_WRONG_AAD_SIZE, _("Wrong AAD size")); return NULL; } tmp = pv_gbytes_memcpy(&cpl->data.confidential_area, sizeof(cpl->data.confidential_area), decr, &copied); if (!tmp || copied != sizeof(cpl->data.confidential_area)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_WRONG_CONFIDENTIAL_SIZE, _("Wrong confidential data size")); return NULL; } tmp = pv_gbytes_memcpy(&cpl->data.tag, sizeof(cpl->data.tag), tag, &copied); if (!tmp || copied != sizeof(cpl->data.tag)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_WRONG_TAG_SIZE, _("Wrong tag size")); return NULL; } return g_steal_pointer(&cpl); } static long pv_cpu_note_data_get_version(GBytes *note, GError **error) { size_t size; const uint32_t *version = g_bytes_get_data(note, &size); STATIC_ASSERT(offsetof(pv_cpu_dump_aad_v1_t, version) == 0); /* check whether we can dereference @version */ if (sizeof(*version) > size) { g_set_error( error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_WRONG_NOTE_SIZE, "%s", _("Confidential CPU data has incorrect size. Dump probably corrupted.")); return -1; } return *version; } dfi_cpu_t *pv_decrypt_cpu_note_data(const unsigned int expected_version, GBytes *cpu_note, GBytes *dump_key, GError **error) { long version = pv_cpu_note_data_get_version(cpu_note, error); if (version < 0) return NULL; if (version != expected_version) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_UNSUPP_SEC_CPU_VER, _("Wrong NT_S390_PV_CPU_DATA version (%ld)"), version); return NULL; } switch (version) { case PV_SEC_CPU_DATA_VERSION_1: { const pv_cpu_dump_confidential_area_v1_t *pv_cpu; const pv_cpu_dump_v1_t *cpu_encrypted; size_t cpu_note_size, cpu_decrypted_size; g_autoptr(GBytes) cpu_decrypted = NULL; cpu_encrypted = g_bytes_get_data(cpu_note, &cpu_note_size); if (sizeof(*cpu_encrypted) > cpu_note_size) { g_set_error( error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_WRONG_NOTE_SIZE, _("Confidential CPU data has incorrect size. Dump probably corrupted.")); return NULL; } cpu_decrypted = pv_decrypt_cpu_dump_area_v1(cpu_encrypted, dump_key, error); if (!cpu_decrypted) { g_prefix_error( error, _("Unable to authenticate confidential CPU data. Dump probably corrupted:" ERR_NEWLINE)); return NULL; } pv_cpu = g_bytes_get_data(cpu_decrypted, &cpu_decrypted_size); if (cpu_decrypted_size != sizeof(*pv_cpu)) { g_set_error( error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_WRONG_NOTE_SIZE, _("Confidential CPU data has incorrect size. Dump probably corrupted.")); return NULL; } /* Check dump flags */ if (pv_cpu->has_osii) util_log_print(UTIL_LOG_WARN, _("CPU state may contain partial instruction results")); return pv_dfi_cpu_from_pv_cpu(pv_cpu); } default: g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_UNSUPP_SEC_CPU_VER, _("Unsupported NT_S390_PV_CPU_DATA version (%ld)"), version); return NULL; } } GBytes *pv_decrypt_cpu_dump_area_v1(const pv_cpu_dump_v1_t *cpu_dump_area, GBytes *dump_key, GError **error) { g_autoptr(GBytes) encr = NULL, aad = NULL, tag = NULL; g_autoptr(GBytes) iv = NULL, out = NULL; PvCipherParms params; encr = g_bytes_new(&cpu_dump_area->confidential_area, sizeof(cpu_dump_area->confidential_area)); aad = g_bytes_new(&cpu_dump_area->aad, sizeof(cpu_dump_area->aad)); tag = g_bytes_new(&cpu_dump_area->tag, sizeof(cpu_dump_area->tag)); iv = g_bytes_new(&cpu_dump_area->aad.iv, sizeof(cpu_dump_area->aad.iv)); params.cipher = PV_DUMP_V1_CIPHER; params.key = dump_key; params.iv = iv; params.tag_size = g_bytes_get_size(tag); if (pv_gcm_decrypt(encr, aad, tag, ¶ms, &out, error) < 0) return NULL; return g_steal_pointer(&out); } dfi_cpu_t *pv_dfi_cpu_from_pv_cpu(const pv_cpu_dump_confidential_area_v1_t *pv_cpu) { g_autoptr(dfi_cpu_t) ret = dfi_cpu_alloc(); STATIC_ASSERT(sizeof(ret->gprs) == sizeof(pv_cpu->gprs)); (void)memcpy(ret->gprs, pv_cpu->gprs, sizeof(ret->gprs)); STATIC_ASSERT(sizeof(ret->psw) == sizeof(pv_cpu->psw)); (void)memcpy(ret->psw, pv_cpu->psw, sizeof(ret->psw)); ret->prefix = pv_cpu->prefix; ret->fpc = pv_cpu->fpc; ret->todpreg = pv_cpu->todpreg; ret->timer = pv_cpu->timer; ret->todcmp = pv_cpu->todcmp; STATIC_ASSERT(sizeof(ret->acrs) == sizeof(pv_cpu->acrs)); (void)memcpy(ret->acrs, pv_cpu->acrs, sizeof(ret->acrs)); STATIC_ASSERT(sizeof(ret->ctrs) == sizeof(pv_cpu->ctrs)); (void)memcpy(ret->ctrs, pv_cpu->ctrs, sizeof(ret->ctrs)); /* Copy floating point register and the high part of the first 16 vector * register */ STATIC_ASSERT(ARRAY_SIZE(ret->fprs) == ARRAY_SIZE(pv_cpu->vector_register_low)); STATIC_ASSERT(ARRAY_SIZE(pv_cpu->vector_register_low) == ARRAY_SIZE(ret->vxrs_low)); dfi_cpu_content_fac_add(DFI_CPU_CONTENT_FAC_VX); for (unsigned int i = 0; i < ARRAY_SIZE(ret->fprs); i++) { ret->fprs[i] = pv_cpu->vector_register_low[i].low; ret->vxrs_low[i] = pv_cpu->vector_register_low[i].high; } STATIC_ASSERT(sizeof(ret->vxrs_high) == sizeof(pv_cpu->vector_register_high)); (void)memcpy(ret->vxrs_high, pv_cpu->vector_register_high, sizeof(ret->vxrs_high)); /* Set guarded storage registers */ ret->reserved = 0; ret->gsd = pv_cpu->gsd; ret->gssm = pv_cpu->gssm; ret->gs_epl_a = pv_cpu->gs_epl_a; /* Add GS facility */ dfi_cpu_content_fac_add(DFI_CPU_CONTENT_FAC_GS); /* NOTE: In the future it might be useful to store `@pv_cpu->dump_flags` * in the `struct dfi_cpu`. Currently, we don't have any use case for * it. */ return g_steal_pointer(&ret); } /* Utilities for decrypting the memory */ struct _pv_crypto_ctx { EVP_CIPHER_CTX *cipher_ctx; BIO *input; BIO *filter; /* To be allocated/deallocated using OpenSSL malloc and clear+free */ pv_tweak_nonce_t *nonce; /* scratch area */ pv_tweak_t tweak_scratch; }; static BIO *pv_BIO_cipher_new(const EVP_CIPHER *cipher, const unsigned char *key, size_t key_len, enum PvCryptoMode mode, GError **error) { bool encrypt = mode == PV_ENCRYPT; EVP_CIPHER_CTX *ctx = NULL; g_autoptr(BIO) ret = NULL; ENGINE *engine = NULL; ret = BIO_new(BIO_f_cipher()); if (!ret) { abort(); return NULL; } if (BIO_get_cipher_ctx(ret, &ctx) != 1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_BIO_FAIL, _("BIO_get_cipher_ctx failed")); return NULL; } g_assert(ctx); if (EVP_CipherInit_ex(ctx, cipher, engine, NULL, NULL, encrypt) != 1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_BIO_FAIL, _("EVP_Cipher_init failed")); return NULL; } /* Check key length */ if (EVP_CIPHER_CTX_key_length(ctx) != (int)key_len) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_BIO_KEY, _("Passed key has incorrect size: %ld != %d"), key_len, EVP_CIPHER_key_length(cipher)); return NULL; } /* Set key */ if (EVP_CipherInit_ex(ctx, NULL, NULL, key, NULL, -1) != 1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_BIO_FAIL, _("EVP_Cipher_init set_key failed")); return NULL; } return g_steal_pointer(&ret); } pv_crypto_ctx_t *pv_crypto_ctx_new(BIO *input, const unsigned char *key, size_t key_size, const pv_tweak_nonce_t *nonce, enum PvCryptoMode mode, GError **error) { g_autoptr(pv_crypto_ctx_t) ret = NULL; g_autoptr(BIO) xts_filter = NULL; g_assert(input); STATIC_ASSERT(sizeof_field(pv_crypto_ctx_t, nonce) == sizeof(nonce)); ret = g_new0(pv_crypto_ctx_t, 1); xts_filter = pv_BIO_cipher_new(EVP_aes_256_xts(), key, key_size, mode, error); if (!xts_filter) { g_prefix_error(error, _("Initializing the zdump crypto context failed" ERR_NEWLINE)); return NULL; } if (BIO_get_cipher_ctx(xts_filter, &ret->cipher_ctx) != 1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_CRYPTO_CTX, _("Initializing the zdump crypto context failed")); return NULL; } g_assert(ret->cipher_ctx); /* set-up BIO chain for the encryption/decryption */ ret->filter = BIO_push(g_steal_pointer(&xts_filter), input); g_assert(ret->filter); if (BIO_up_ref(input) != 1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_CRYPTO_CTX, _("Initializing the zdump crypto context failed")); return NULL; } ret->input = input; ret->nonce = OPENSSL_malloc(sizeof(*ret->nonce)); if (!ret->nonce) abort(); (void)memcpy(ret->nonce, nonce, sizeof(*ret->nonce)); return g_steal_pointer(&ret); } void pv_crypto_ctx_free(pv_crypto_ctx_t *ctx) { if (!ctx) return; g_clear_pointer(&ctx->input, BIO_vfree); g_clear_pointer(&ctx->filter, BIO_vfree); /* It's intentional that we don't free @ctx->cipher_ctx since it's not * owned by us, but the BIO chain */ ctx->cipher_ctx = NULL; OPENSSL_clear_free(ctx->nonce, sizeof(*ctx->nonce)); g_free(ctx); } void calculate_tweak(const pv_tweak_component_t *tweak, const pv_tweak_nonce_t *nonce, pv_tweak_t *out) { for (size_t i = 0; i < ARRAY_SIZE(out->value); i++) out->value[i] = tweak->value[i] | nonce->value[i]; } int pv_read_page(BIO *input, BIO *output, GError **error) { char data[PAGE_SIZE]; int rc; rc = BIO_read(input, data, ARRAY_SIZE(data)); if (rc != ARRAY_SIZE(data)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_BIO, _("BIO_read failed")); return -1; } rc = BIO_write(output, data, ARRAY_SIZE(data)); if (rc != ARRAY_SIZE(data)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_BIO, _("BIO_write failed")); return -1; } return rc; } unsigned long pv_page_state(const pv_tweak_component_t *comp) { unsigned long ret = PV_INVAL_PAGE_STATE; if (comp->special.indicator != PV_SPECIAL_INDICATOR) return PV_ENCRYPTED_PAGE; if (comp->special.flag_reserved1 || comp->special.flag_reserved2 || comp->special.is_zero_page + comp->special.is_shared_page + comp->special.is_mapped_page < 1) return PV_INVAL_PAGE_STATE; if (comp->special.is_zero_page) ret |= PV_ZERO_PAGE; if (comp->special.is_shared_page) ret |= PV_SHARED_PAGE; if (comp->special.is_mapped_page) ret |= PV_MAPPED_PAGE; return ret; } static const unsigned char NULL_DATA[PV_CSS_PAGESIZE] = { 0x0 }; static bool pv_BIO_is_seekable(BIO *bio) { const int type = BIO_method_type(bio); return type == BIO_TYPE_FD || type == BIO_TYPE_FILE; } static int pv_BIO_seek(BIO *bio, long long offset) { /* The documentation of @BIO_seeks says @offset is a `int` but the * source code actually shows it's a long. Therefore add these * additional checks here to detect in case something changes in * OpenSSL. */ #pragma GCC diagnostic push #pragma GCC diagnostic error "-Wconversion" #pragma GCC diagnostic error "-Wsign-conversion" return BIO_seek(bio, offset); #pragma GCC diagnostic pop } static int update_tweak(pv_crypto_ctx_t *crypto_ctx, GError **error) { EVP_CIPHER_CTX *ctx = crypto_ctx->cipher_ctx; u8 *tweak = crypto_ctx->tweak_scratch.value; /* Check tweak length */ if (EVP_CIPHER_CTX_iv_length(ctx) != ARRAY_SIZE(crypto_ctx->tweak_scratch.value)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_TWEAK, _("Tweak has wrong size")); return -1; } /* set the new tweak IV */ if (EVP_CipherInit_ex(ctx, NULL, NULL, NULL, tweak, -1) != 1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_TWEAK, _("Initializing tweaks failed")); return -1; } return 0; } ssize_t pv_process_pglist(pv_crypto_ctx_t *ctx, BIO *output, const pv_tweak_component_t *tweak_components, size_t tweak_components_len, long input_off, GError **error) { bool is_output_seekable = pv_BIO_is_seekable(output); long cur_in_off = input_off, cur_out_off = 0; ssize_t page_idx; int rc; assert(ctx->input); assert(ctx->filter); assert(output); assert(tweak_components_len <= SSIZE_MAX); /* See https://www.openssl.org/docs/man1.1.0/man3/BIO_seek.html */ if (!pv_BIO_is_seekable(ctx->input)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_BIO, _("Input source is not seekable")); return -1; } if (pv_BIO_seek(ctx->input, cur_in_off) == -1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_BIO, _("BIO_seek failed")); return -1; } if (tweak_components_len > LONG_MAX / PV_CSS_PAGESIZE) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PAGELIST_ELF_OFFSET_TOO_LARGE, _("Possible long overflow detected: Try to read %li pages"), tweak_components_len); } for (page_idx = 0; page_idx < (ssize_t)tweak_components_len; page_idx++) { const pv_tweak_component_t *tweak_comp = &tweak_components[page_idx]; unsigned long page_state; BIO *input = NULL; g_assert_nonnull(tweak_comp); page_state = pv_page_state(tweak_comp); if (page_state & PV_INVAL_PAGE_STATE) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_BIO, _("Invalid page state")); return -1; } /* g_assert(page_state & PV_MAPPED_PAGE); */ page_state &= ~PV_MAPPED_PAGE; if (page_state & PV_ZERO_PAGE) { /* Nothing to do here for BIO_FILE because a sparse file * is filled with zeros by default. Therefore we can * simply calculate the new output offset. For a * BIO_s_mem BIO_seek doesn't work therefore we've to * work around. */ cur_out_off += PV_CSS_PAGESIZE; cur_in_off += PV_CSS_PAGESIZE; if (G_UNLIKELY(pv_BIO_seek(ctx->input, cur_in_off) == -1)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_BIO, _("BIO_seek failed")); return -1; } if (is_output_seekable) { if (G_UNLIKELY(pv_BIO_seek(output, cur_out_off) == -1)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_BIO, _("BIO_seek failed")); return -1; } } else { if (G_UNLIKELY( BIO_write(output, NULL_DATA, ARRAY_SIZE(NULL_DATA)) != ARRAY_SIZE(NULL_DATA))) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_BIO, _("BIO_write failed")); return -1; } } if (page_state & ~PV_ZERO_PAGE) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_INVAL_STATE, _("Invalid page state. page-idx: %#lx, state: %#lx"), page_idx, page_state); return -1; } continue; } if (page_state & PV_SHARED_PAGE) { /* shared pages are not encrypted */ input = ctx->input; if (page_state & ~PV_SHARED_PAGE) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_INVAL_STATE, _("Invalid page state. page-idx: %#lx, state: %#lx"), page_idx, page_state); return -1; } } else if (page_state & PV_ENCRYPTED_PAGE) { input = ctx->filter; calculate_tweak(tweak_comp, ctx->nonce, &ctx->tweak_scratch); /* set new tweak */ if (update_tweak(ctx, error) < 0) return -1; if (page_state & ~PV_ENCRYPTED_PAGE) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_INVAL_STATE, _("Invalid page state. page-idx: %#lx, state: %#lx"), page_idx, page_state); return -1; } } else { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PGLIST_INVAL_STATE, _("Invalid page state. page-idx: %#lx, state: %#lx"), page_idx, page_state); return -1; } rc = pv_read_page(input, output, error); if (rc != PV_CSS_PAGESIZE) return -1; /* adapt the offsets */ cur_in_off += rc; cur_out_off += rc; } return page_idx; } bool pv_is_pv_elf(const Elf64_Shdr *shdrs, const unsigned int shnum, const char *shstrtab, const size_t shstrtab_size) { return find_elf_shdr_by_name(shdrs, shnum, shstrtab, shstrtab_size, PV_ELF_SECTION_NAME_COMPL) != NULL; } struct _storage_state_mmap { void *first_page_ptr; size_t mapped_size; pv_tweak_component_t *tweak_components; size_t num_tweaks; gatomicrefcount ref_count; }; storage_state_mmap_t *storage_state_mmap_new(const int fd, const size_t file_size, const u64 offset, const u64 size, GError **error) { size_t tweak_components_cnt, start_addr, min_size, in_page_offset, mmapped_size; g_autoptr(storage_state_mmap_t) ret = NULL; int saved_errno = 0; u8 *ptr; if (size == 0 || size % sizeof(pv_tweak_component_t) != 0) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_MMAP, _("StorageState MMAP: size (%#llx) not a multiple of (%#lx)"), size, sizeof(pv_tweak_component_t)); return NULL; } start_addr = page_start_addr(page_index(offset), error); if (*error) return NULL; tweak_components_cnt = size / sizeof(pv_tweak_component_t); in_page_offset = page_offset(offset); mmapped_size = size + in_page_offset; if (start_addr > SSIZE_MAX) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_MMAP, _("StorageState MMAP: page start address is too large (%#lx)"), start_addr); return NULL; } if (G_UNLIKELY(!g_uint64_checked_add(&min_size, start_addr, mmapped_size))) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PAGE_END_ADDR_OVERFLOW, _("UInt overflow detected: %s: start_addr %#lx mmap_size %#lx"), __func__, start_addr, mmapped_size); return NULL; } if (file_size < min_size) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_MMAP, _("mmap failed: file too small")); return NULL; } ptr = mmap(NULL, mmapped_size, PROT_READ, MAP_POPULATE | MAP_PRIVATE, fd, (ssize_t)start_addr); saved_errno = errno; if (ptr == MAP_FAILED) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_MMAP, _("mmap failed: %s"), g_strerror(saved_errno)); return NULL; } ret = g_new0(typeof(*ret), 1); ret->first_page_ptr = ptr; ret->tweak_components = (pv_tweak_component_t *)(ptr + in_page_offset); ret->num_tweaks = tweak_components_cnt; ret->mapped_size = mmapped_size; g_atomic_ref_count_init(&ret->ref_count); return g_steal_pointer(&ret); } storage_state_mmap_t *storage_state_mmap_ref(storage_state_mmap_t *storage_state) { g_assert(storage_state); g_atomic_ref_count_inc(&storage_state->ref_count); return storage_state; } void storage_state_mmap_unref(storage_state_mmap_t *storage_state) { if (!storage_state) return; if (storage_state->ref_count && !g_atomic_ref_count_dec(&storage_state->ref_count)) return; if (storage_state->first_page_ptr) { int rc = munmap(storage_state->first_page_ptr, storage_state->mapped_size); if (rc != 0) util_log_print(UTIL_LOG_WARN, _("munmap has failed")); } g_free(storage_state); } pv_elf_ctx_t *pv_elf_ctx_new(const int fd, const pv_dump_completion_confidential_area_v1_t *cpl_conf, storage_state_mmap_t *storage_state_data, const u64 elf_load_offset, GError **error) { g_autoptr(pv_elf_ctx_t) ret = NULL; g_autoptr(BIO) input = NULL; input = BIO_new_fd(fd, BIO_NOCLOSE); if (!input) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_CTX_BIO, _("cannot open file")); return NULL; } ret = g_new0(typeof(*ret), 1); ret->pv_ctx = pv_crypto_ctx_new(input, cpl_conf->key, sizeof(cpl_conf->key), &cpl_conf->nonce, PV_DECRYPT, error); if (!ret->pv_ctx) return NULL; ret->storage_state_data = storage_state_mmap_ref(storage_state_data); ret->elf_load_off = elf_load_offset; return g_steal_pointer(&ret); } void pv_elf_ctx_free(pv_elf_ctx_t *p) { if (!p) return; g_clear_pointer(&p->pv_ctx, pv_crypto_ctx_free); g_clear_pointer(&p->storage_state_data, storage_state_mmap_unref); g_clear_pointer(&p->output, BIO_vfree); g_free(p); } const pv_tweak_component_t *pv_get_tweak_components(storage_state_mmap_t *storage_state_data, u64 page_idx, u64 page_cnt) { u64 last_page_idx; g_assert_cmpuint(page_cnt, >=, 1); if (!u64_checked_add(&last_page_idx, page_idx, page_cnt - 1)) return NULL; if (last_page_idx >= storage_state_data->num_tweaks) return NULL; return &storage_state_data->tweak_components[page_idx]; } /* Return version number if possible */ static long completion_data_get_version(GBytes *cpl_data, GError **error) { size_t size; const uint32_t *version = g_bytes_get_data(cpl_data, &size); STATIC_ASSERT(offsetof(pv_dump_completion_aad_v1_t, version) == 0); /* check whether we can dereference @version */ if (sizeof(*version) > size) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_UNSUPP_COMPL_VER, _("Unsupported " PV_ELF_SECTION_NAME_COMPL " section size (%#lx)"), size); return -1; } return *version; } int pv_process_section_data(const int fd, const size_t file_size, GBytes *completion_sec, const u64 storage_state_offset, const size_t storage_state_size, GBytes *cck, pv_dump_completion_t **completion_decr, GBytes **dump_key, storage_state_mmap_t **storage_state, GError **error) { g_autoptr(pv_dump_completion_t) _completion_decr = NULL; g_autoptr(storage_state_mmap_t) _storage_state_data = NULL; g_autoptr(GBytes) _dump_key = NULL; g_assert(completion_sec); long version; version = completion_data_get_version(completion_sec, error); if (version < 0) return -1; switch (version) { case PV_COMPL_DATA_VERSION_1: { const pv_dump_completion_data_v1_t *ccd; size_t size; ccd = g_bytes_get_data(completion_sec, &size); if (sizeof(*ccd) > size) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_CORRUPTED_COMPL_DATA, _("Corrupted completion configuration data")); return -1; } if (ccd->aad.len != sizeof(*ccd)) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_CORRUPTED_COMPL_DATA, _("Incorrect completion configuration data length")); return -1; } _dump_key = pv_derive_dump_key_v1(ccd, cck, error); if (!_dump_key) { g_prefix_error(error, _("Unable to derive dump key: ")); return -1; } _completion_decr = (pv_dump_completion_t *)pv_decrypt_dump_completion_v1( ccd, _dump_key, error); if (!_completion_decr) { g_prefix_error(error, _("Unable to decrypt completion configuration data: ")); return -1; } break; } default: g_set_error( error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_UNSUPP_COMPL_VER, _("Unsupported dump completion version (%#lx) found in section " PV_ELF_SECTION_NAME_COMPL), version); return -1; } _storage_state_data = storage_state_mmap_new(fd, file_size, storage_state_offset, storage_state_size, error); if (!_storage_state_data) return -1; *dump_key = g_steal_pointer(&_dump_key); *completion_decr = g_steal_pointer(&_completion_decr); *storage_state = g_steal_pointer(&_storage_state_data); return 0; } int pv_elf_read(const pv_elf_ctx_t *elf_ctx, const u64 start_addr, void *dst, const u64 size, GError **error) { u64 pglist_size, pglist_start_idx, pglist_end_idx, pglist_num_pages; const pv_tweak_component_t *pglist_tweak_components; u64 pglist_start_addr, pglist_end_addr, pglist_elf_off, page_off; const unsigned char *data = NULL; g_autoptr(BIO) output = NULL; gssize num_processed_pages; long data_size; u64 end_addr; /* nothing to do then */ if (size == 0) return 0; end_addr = start_addr + size - 1; if (end_addr < start_addr) { g_set_error( error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_READ_END_ADDR_OVERFLOW, _("UInt Overflow during reading memory detected. start_addr %p, size: %#llx"), (void *)start_addr, size); return -1; } page_off = page_offset(start_addr); pglist_start_idx = page_index(start_addr); pglist_start_addr = page_start_addr(pglist_start_idx, error); /* Could never happen here */ if (*error) return -1; pglist_end_idx = page_index(end_addr); pglist_end_addr = page_end_addr(pglist_end_idx, error); if (*error) return -1; pglist_size = pglist_end_addr - pglist_start_addr + 1; pglist_num_pages = pglist_end_idx - pglist_start_idx + 1; g_assert(IS_ALIGNED(pglist_start_addr, PV_CSS_PAGESIZE)); g_assert(IS_ALIGNED(pglist_end_addr + 1, PV_CSS_PAGESIZE)); g_assert(IS_ALIGNED(pglist_size, PV_CSS_PAGESIZE)); /* must be true, is max UINT64_T/0x1000 + 1 < SSIZE_MAX */ g_assert(pglist_num_pages <= SSIZE_MAX); g_assert(pglist_num_pages > 0); pglist_tweak_components = pv_get_tweak_components(elf_ctx->storage_state_data, pglist_start_idx, pglist_num_pages); if (!pglist_tweak_components) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_READ, _("Page tweaks were not found idx %llu num %llu"), pglist_start_idx, pglist_num_pages); return -1; } output = BIO_new(BIO_s_mem()); if (!output) abort(); if (!u64_checked_add(&pglist_elf_off, elf_ctx->elf_load_off, pglist_start_addr)) { g_set_error( error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PAGELIST_ELF_OFFSET_TOO_LARGE, _("UInt overflow detected: ELF load offset %#llx page start address %#llx"), elf_ctx->elf_load_off, pglist_start_addr); return -1; } if (pglist_elf_off > LONG_MAX) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_PAGELIST_ELF_OFFSET_TOO_LARGE, _("ELF load offset is too large")); return -1; } /* Process the pages - in this case do the decryption */ num_processed_pages = pv_process_pglist(elf_ctx->pv_ctx, output, pglist_tweak_components, pglist_num_pages, (long)pglist_elf_off, error); if (num_processed_pages < 0) return -1; if (num_processed_pages != (gssize)pglist_num_pages) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_READ, _("Processed page count isn't correct")); return -1; } if (BIO_flush(output) != 1) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_READ, _("BIO_flush failed")); return -1; } data_size = BIO_get_mem_data(output, &data); if (data_size < 0 || !data) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_READ, _("BIO_get_mem_data failed")); return -1; } if ((u64)data_size != pglist_size) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_READ, _("Decrypting memory failed")); return -1; } /* NOTE previous assertions/overflow checks assure that this can never happen. * We keep this to be extra sure and protect the following memcpy from * malicious copying. */ if (page_off + size > (u64)data_size) { g_set_error(error, ZDUMP_PV_UTILS_ERROR, ZDUMP_ERR_ELF_READ, _("%s: Illegal state"), __func__); return -1; } (void)memcpy(dst, data + page_off, size); return 0; }