diff options
Diffstat (limited to 'src/installer/corehost/cli/apphost/bundle')
17 files changed, 1139 insertions, 0 deletions
diff --git a/src/installer/corehost/cli/apphost/bundle/dir_utils.cpp b/src/installer/corehost/cli/apphost/bundle/dir_utils.cpp new file mode 100644 index 00000000000..eede9529812 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/dir_utils.cpp @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "dir_utils.h" +#include "error_codes.h" +#include "utils.h" + +using namespace bundle; + +bool dir_utils_t::has_dirs_in_path(const pal::string_t& path) +{ + return path.find_last_of(DIR_SEPARATOR) != pal::string_t::npos; +} + +void dir_utils_t::create_directory_tree(const pal::string_t &path) +{ + if (path.empty()) + { + return; + } + + if (pal::directory_exists(path)) + { + return; + } + + if (has_dirs_in_path(path)) + { + create_directory_tree(get_directory(path)); + } + + if (!pal::mkdir(path.c_str(), 0700)) // Owner - rwx + { + if (pal::directory_exists(path)) + { + // The directory was created since we last checked. + return; + } + + trace::error(_X("Failure processing application bundle.")); + trace::error(_X("Failed to create directory [%s] for extracting bundled files."), path.c_str()); + throw StatusCode::BundleExtractionIOError; + } +} + +void dir_utils_t::remove_directory_tree(const pal::string_t& path) +{ + if (path.empty()) + { + return; + } + + std::vector<pal::string_t> dirs; + pal::readdir_onlydirectories(path, &dirs); + + for (const pal::string_t &dir : dirs) + { + pal::string_t dir_path = path; + append_path(&dir_path, dir.c_str()); + + remove_directory_tree(dir_path); + } + + std::vector<pal::string_t> files; + pal::readdir(path, &files); + + for (const pal::string_t &file : files) + { + pal::string_t file_path = path; + append_path(&file_path, file.c_str()); + + if (!pal::remove(file_path.c_str())) + { + trace::warning(_X("Failed to remove temporary file [%s]."), file_path.c_str()); + } + } + + if (!pal::rmdir(path.c_str())) + { + trace::warning(_X("Failed to remove temporary directory [%s]."), path.c_str()); + } +} + +// Fixup a path to have current platform's directory separator. +void dir_utils_t::fixup_path_separator(pal::string_t& path) +{ + const pal::char_t bundle_dir_separator = '/'; + + if (bundle_dir_separator != DIR_SEPARATOR) + { + for (size_t pos = path.find(bundle_dir_separator); + pos != pal::string_t::npos; + pos = path.find(bundle_dir_separator, pos)) + { + path[pos] = DIR_SEPARATOR; + } + } +} + +// Retry the rename operation with some wait in between the attempts. +// This is an attempt to workaround for possible file locking caused by AV software. + +bool dir_utils_t::rename_with_retries(pal::string_t& old_name, pal::string_t& new_name, bool& dir_exists) +{ + for (int retry_count=0; retry_count < 500; retry_count++) + { + if (pal::rename(old_name.c_str(), new_name.c_str()) == 0) + { + return true; + } + bool should_retry = errno == EACCES; + + if (pal::directory_exists(new_name)) + { + // Check directory_exists() on each run, because a concurrent process may have + // created the new_name directory. + // + // The rename() operation above fails with errono == EACCESS if + // * Directory new_name already exists, or + // * Paths are invalid paths, or + // * Due to locking/permission problems. + // Therefore, we need to perform the directory_exists() check again. + + dir_exists = true; + return false; + } + + if (should_retry) + { + trace::info(_X("Retrying Rename [%s] to [%s] due to EACCES error"), old_name.c_str(), new_name.c_str()); + pal::sleep(100); + continue; + } + else + { + return false; + } + } + + return false; +} diff --git a/src/installer/corehost/cli/apphost/bundle/dir_utils.h b/src/installer/corehost/cli/apphost/bundle/dir_utils.h new file mode 100644 index 00000000000..ffe0acffb60 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/dir_utils.h @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __DIR_UTIL_H__ +#define __DIR_UTIL_H__ + +#include <cstdint> +#include "pal.h" + +namespace bundle +{ + class dir_utils_t + { + public: + static bool has_dirs_in_path(const pal::string_t &path); + static void remove_directory_tree(const pal::string_t &path); + static void create_directory_tree(const pal::string_t &path); + static void fixup_path_separator(pal::string_t& path); + static bool rename_with_retries(pal::string_t& old_name, pal::string_t& new_name, bool &new_dir_exists); + }; +} + +#endif // __DIR_UTIL_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/extractor.cpp b/src/installer/corehost/cli/apphost/bundle/extractor.cpp new file mode 100644 index 00000000000..71bb2259e9d --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/extractor.cpp @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "extractor.h" +#include "error_codes.h" +#include "dir_utils.h" +#include "pal.h" +#include "utils.h" + +using namespace bundle; + +pal::string_t& extractor_t::extraction_dir() +{ + if (m_extraction_dir.empty()) + { + // Compute the final extraction location as: + // m_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<id>/... + // + // If DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set in the environment, + // a default is choosen within the temporary directory. + + if (!pal::getenv(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR"), &m_extraction_dir)) + { + if (!pal::get_default_bundle_extraction_base_dir(m_extraction_dir)) + { + trace::error(_X("Failure processing application bundle.")); + trace::error(_X("Failed to determine location for extracting embedded files.")); + trace::error(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set, and a read-write temp-directory couldn't be created.")); + throw StatusCode::BundleExtractionFailure; + } + } + + pal::string_t host_name = strip_executable_ext(get_filename(m_bundle_path)); + append_path(&m_extraction_dir, host_name.c_str()); + append_path(&m_extraction_dir, m_bundle_id.c_str()); + + trace::info(_X("Files embedded within the bundled will be extracted to [%s] directory."), m_extraction_dir.c_str()); + } + + return m_extraction_dir; +} + +pal::string_t& extractor_t::working_extraction_dir() +{ + if (m_working_extraction_dir.empty()) + { + // Compute the working extraction location for this process, + // before the extracted files are committed to the final location + // working_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<proc-id-hex> + + m_working_extraction_dir = get_directory(extraction_dir()); + pal::char_t pid[32]; + pal::snwprintf(pid, 32, _X("%x"), pal::get_pid()); + append_path(&m_working_extraction_dir, pid); + + trace::info(_X("Temporary directory used to extract bundled files is [%s]."), m_working_extraction_dir.c_str()); + } + + return m_working_extraction_dir; +} + +// Create a file to be extracted out on disk, including any intermediate sub-directories. +FILE* extractor_t::create_extraction_file(const pal::string_t& relative_path) +{ + pal::string_t file_path = working_extraction_dir(); + append_path(&file_path, relative_path.c_str()); + + // working_extraction_dir is assumed to exist, + // so we only create sub-directories if relative_path contains directories + if (dir_utils_t::has_dirs_in_path(relative_path)) + { + dir_utils_t::create_directory_tree(get_directory(file_path)); + } + + FILE* file = pal::file_open(file_path.c_str(), _X("wb")); + + if (file == nullptr) + { + trace::error(_X("Failure processing application bundle.")); + trace::error(_X("Failed to open file [%s] for writing."), file_path.c_str()); + throw StatusCode::BundleExtractionIOError; + } + + return file; +} + +// Extract one file from the bundle to disk. +void extractor_t::extract(const file_entry_t &entry, reader_t &reader) +{ + FILE* file = create_extraction_file(entry.relative_path()); + reader.set_offset(entry.offset()); + size_t size = entry.size(); + + if (fwrite(reader, 1, size, file) != size) + { + trace::error(_X("Failure extracting contents of the application bundle.")); + trace::error(_X("I/O failure when writing extracted files.")); + throw StatusCode::BundleExtractionIOError; + } + + fclose(file); +} + +void extractor_t::begin() +{ + // Files are extracted to a specific deterministic location on disk + // on first run, and are available for reuse by subsequent similar runs. + // + // The extraction should be fault tolerant with respect to: + // * Failures/crashes during extraction which result in partial-extraction + // * Race between two or more processes concurrently attempting extraction + // + // In order to solve these issues, we implement a extraction as a two-phase approach: + // 1) Files embedded in a bundle are extracted to a process-specific temporary + // extraction location (working_extraction_dir) + // 2) Upon successful extraction, working_extraction_dir is renamed to the actual + // extraction location (extraction_dir) + // + // This effectively creates a file-lock to protect against races and failed extractions. + + + dir_utils_t::create_directory_tree(working_extraction_dir()); +} + +void extractor_t::clean() +{ + dir_utils_t::remove_directory_tree(working_extraction_dir()); +} + +void extractor_t::commit_dir() +{ + // Commit an entire new extraction to the final extraction directory + // Retry the move operation with some wait in between the attempts. This is to workaround for possible file locking + // caused by AV software. Basically the extraction process above writes a bunch of executable files to disk + // and some AV software may decide to scan them on write. If this happens the files will be locked which blocks + // our ablity to move them. + + bool extracted_by_concurrent_process = false; + bool extracted_by_current_process = + dir_utils_t::rename_with_retries(working_extraction_dir(), extraction_dir(), extracted_by_concurrent_process); + + if (extracted_by_concurrent_process) + { + // Another process successfully extracted the dependencies + trace::info(_X("Extraction completed by another process, aborting current extraction.")); + clean(); + } + + if (!extracted_by_current_process && !extracted_by_concurrent_process) + { + trace::error(_X("Failure processing application bundle.")); + trace::error(_X("Failed to commit extracted files to directory [%s]."), extraction_dir().c_str()); + throw StatusCode::BundleExtractionFailure; + } + + trace::info(_X("Completed new extraction.")); +} + +void extractor_t::commit_file(const pal::string_t& relative_path) +{ + // Commit individual files to the final extraction directory. + + pal::string_t working_file_path = working_extraction_dir(); + append_path(&working_file_path, relative_path.c_str()); + + pal::string_t final_file_path = extraction_dir(); + append_path(&final_file_path, relative_path.c_str()); + + if (dir_utils_t::has_dirs_in_path(relative_path)) + { + dir_utils_t::create_directory_tree(get_directory(final_file_path)); + } + + bool extracted_by_concurrent_process = false; + bool extracted_by_current_process = + dir_utils_t::rename_with_retries(working_file_path, final_file_path, extracted_by_concurrent_process); + + if (extracted_by_concurrent_process) + { + // Another process successfully extracted the dependencies + trace::info(_X("Extraction completed by another process, aborting current extraction.")); + } + + if (!extracted_by_current_process && !extracted_by_concurrent_process) + { + trace::error(_X("Failure processing application bundle.")); + trace::error(_X("Failed to commit extracted files to directory [%s]."), extraction_dir().c_str()); + throw StatusCode::BundleExtractionFailure; + } + + trace::info(_X("Extraction recovered [%s]"), relative_path.c_str()); +} + +void extractor_t::extract_new(reader_t& reader) +{ + begin(); + for (const file_entry_t& entry : m_manifest.files) + { + extract(entry, reader); + } + commit_dir(); +} + +// Verify an existing extraction contains all files listed in the bundle manifest. +// If some files are missing, extract them individually. +void extractor_t::verify_recover_extraction(reader_t& reader) +{ + pal::string_t& ext_dir = extraction_dir(); + bool recovered = false; + + for (const file_entry_t& entry : m_manifest.files) + { + pal::string_t file_path = ext_dir; + append_path(&file_path, entry.relative_path().c_str()); + + if (!pal::file_exists(file_path)) + { + if (!recovered) + { + recovered = true; + begin(); + } + + extract(entry, reader); + commit_file(entry.relative_path()); + } + } + + if (recovered) + { + clean(); + } +} + +pal::string_t& extractor_t::extract(reader_t& reader) +{ + if (pal::directory_exists(extraction_dir())) + { + trace::info(_X("Reusing existing extraction of application bundle.")); + verify_recover_extraction(reader); + } + else + { + trace::info(_X("Starting new extraction of application bundle.")); + extract_new(reader); + } + + return m_extraction_dir; +} diff --git a/src/installer/corehost/cli/apphost/bundle/extractor.h b/src/installer/corehost/cli/apphost/bundle/extractor.h new file mode 100644 index 00000000000..73b75ecdd09 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/extractor.h @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __EXTRACTOR_H__ +#define __EXTRACTOR_H__ + +#include "reader.h" +#include "manifest.h" + +namespace bundle +{ + class extractor_t + { + public: + extractor_t(const pal::string_t &bundle_id, + const pal::string_t& bundle_path, + const manifest_t &manifest) + :m_extraction_dir(), + m_working_extraction_dir(), + m_manifest(manifest) + { + m_bundle_id = bundle_id; + m_bundle_path = bundle_path; + } + + pal::string_t& extract(reader_t& reader); + + private: + pal::string_t& extraction_dir(); + pal::string_t& working_extraction_dir(); + + void extract_new(reader_t& reader); + void verify_recover_extraction(reader_t& reader); + + FILE* create_extraction_file(const pal::string_t& relative_path); + void extract(const file_entry_t& entry, reader_t& reader); + + void begin(); + void commit_file(const pal::string_t& relative_path); + void commit_dir(); + void clean(); + + pal::string_t m_bundle_id; + pal::string_t m_bundle_path; + pal::string_t m_extraction_dir; + pal::string_t m_working_extraction_dir; + const manifest_t& m_manifest; + }; +} + +#endif // __EXTRACTOR_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/file_entry.cpp b/src/installer/corehost/cli/apphost/bundle/file_entry.cpp new file mode 100644 index 00000000000..ba4b235fa93 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/file_entry.cpp @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "file_entry.h" +#include "trace.h" +#include "dir_utils.h" +#include "error_codes.h" + +using namespace bundle; + +bool file_entry_t::is_valid() const +{ + return m_offset > 0 && m_size >= 0 && + static_cast<file_type_t>(m_type) < file_type_t::__last; +} + +file_entry_t file_entry_t::read(reader_t &reader) +{ + // First read the fixed-sized portion of file-entry + const file_entry_fixed_t* fixed_data = reinterpret_cast<const file_entry_fixed_t*>(reader.read_direct(sizeof(file_entry_fixed_t))); + file_entry_t entry(fixed_data); + + if (!entry.is_valid()) + { + trace::error(_X("Failure processing application bundle; possible file corruption.")); + trace::error(_X("Invalid FileEntry detected.")); + throw StatusCode::BundleExtractionFailure; + } + + reader.read_path_string(entry.m_relative_path); + dir_utils_t::fixup_path_separator(entry.m_relative_path); + + return entry; +} diff --git a/src/installer/corehost/cli/apphost/bundle/file_entry.h b/src/installer/corehost/cli/apphost/bundle/file_entry.h new file mode 100644 index 00000000000..efe405cc9a6 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/file_entry.h @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __FILE_ENTRY_H__ +#define __FILE_ENTRY_H__ + +#include "file_type.h" +#include "reader.h" + +namespace bundle +{ + // FileEntry: Records information about embedded files. + // + // The bundle manifest records the following meta-data for each + // file embedded in the bundle: + // Fixed size portion (file_entry_fixed_t) + // - Offset + // - Size + // - File Entry Type + // Variable Size portion + // - relative path (7-bit extension encoded length prefixed string) + +#pragma pack(push, 1) + struct file_entry_fixed_t + { + int64_t offset; + int64_t size; + file_type_t type; + }; +#pragma pack(pop) + + class file_entry_t + { + public: + file_entry_t() + : m_offset(0) + , m_size(0) + , m_type(file_type_t::__last) + , m_relative_path() + { + } + + file_entry_t(const file_entry_fixed_t *fixed_data) + :m_relative_path() + { + // File_entries in the bundle-manifest are expected to be used + // beyond startup (for loading files directly from bundle, lazy extraction, etc.). + // The contents of fixed_data are copied on to file_entry in order to + // avoid memory mapped IO later. + + m_offset = fixed_data->offset; + m_size = fixed_data->size; + m_type = fixed_data->type; + } + + const pal::string_t relative_path() const { return m_relative_path; } + int64_t offset() const { return m_offset; } + int64_t size() const { return m_size; } + file_type_t type() const { return m_type; } + + static file_entry_t read(reader_t &reader); + + private: + int64_t m_offset; + int64_t m_size; + file_type_t m_type; + pal::string_t m_relative_path; // Path of an embedded file, relative to the extraction directory. + bool is_valid() const; + }; +} +#endif // __FILE_ENTRY_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/file_type.h b/src/installer/corehost/cli/apphost/bundle/file_type.h new file mode 100644 index 00000000000..acbfca06859 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/file_type.h @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __FILE_TYPE_H__ +#define __FILE_TYPE_H__ + +#include <cstdint> + +namespace bundle +{ + // FileType: Identifies the type of file embedded into the bundle. + // + // The bundler differentiates a few kinds of files via the manifest, + // with respect to the way in which they'll be used by the runtime. + // + // Currently all files are extracted out to the disk, but future + // implementations will process certain file_types directly from the bundle. + + enum file_type_t : uint8_t + { + unknown, + assembly, + native_binary, + deps_json, + runtime_config_json, + symbols, + __last + }; +} + +#endif // __FILE_TYPE_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/header.cpp b/src/installer/corehost/cli/apphost/bundle/header.cpp new file mode 100644 index 00000000000..23695bb93f1 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/header.cpp @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "header.h" +#include "reader.h" +#include "error_codes.h" +#include "trace.h" + +using namespace bundle; + +// The AppHost expects the bundle_header to be an exact_match for which it was built. +// The framework accepts backwards compatible header versions. +bool header_fixed_t::is_valid(bool exact_match) const +{ + if (num_embedded_files <= 0) + { + return false; + } + + if (exact_match) + { + return (major_version == header_t::major_version) && (minor_version == header_t::minor_version); + } + + return ((major_version < header_t::major_version) || + (major_version == header_t::major_version && minor_version <= header_t::minor_version)); +} + +header_t header_t::read(reader_t& reader, bool need_exact_version) +{ + const header_fixed_t* fixed_header = reinterpret_cast<const header_fixed_t*>(reader.read_direct(sizeof(header_fixed_t))); + + if (!fixed_header->is_valid(need_exact_version)) + { + trace::error(_X("Failure processing application bundle.")); + trace::error(_X("Bundle header version compatibility check failed.")); + + throw StatusCode::BundleExtractionFailure; + } + + header_t header(fixed_header->num_embedded_files); + + // bundle_id is a component of the extraction path + reader.read_path_string(header.m_bundle_id); + + if (fixed_header->major_version > 1) + { + header.m_v2_header = reinterpret_cast<const header_fixed_v2_t*>(reader.read_direct(sizeof(header_fixed_v2_t))); + } + + return header; +} diff --git a/src/installer/corehost/cli/apphost/bundle/header.h b/src/installer/corehost/cli/apphost/bundle/header.h new file mode 100644 index 00000000000..1b1696219dc --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/header.h @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __HEADER_H__ +#define __HEADER_H__ + +#include <cstdint> +#include "pal.h" +#include "reader.h" + +namespace bundle +{ + // The Bundle Header (v1) + // Fixed size thunk (header_fixed_t) + // - Major Version + // - Minor Version + // - Number of embedded files + // Variable size portion: + // - Bundle ID (7-bit extension encoded length prefixed string) + // The Bundle Header (v2) [additional content] + // Fixed size thunk (header_fixed_v2_t) + // - DepsJson Location (Offset, Size) + // - RuntimeConfig Location (Offset, Size) + // - Flags + +#pragma pack(push, 1) + struct header_fixed_t + { + public: + uint32_t major_version; + uint32_t minor_version; + int32_t num_embedded_files; + + bool is_valid(bool exact_match = false) const; + }; +#pragma pack(pop) + + // netcoreapp3_compat_mode flag is set on a .net5 app, which chooses to build single-file apps in .netcore3.x compat mode, + // This indicates that: + // All published files are bundled into the app; some of them will be extracted to disk. + // AppContext.BaseDirectory is set to the extraction directory (and not the AppHost directory). + enum header_flags_t : uint64_t + { + none = 0, + netcoreapp3_compat_mode = 1 + }; + +#pragma pack(push, 1) + struct location_t + { + public: + int64_t offset; + int64_t size; + }; + + // header_fixed_v2_t is available in single-file apps targetting .net5+ frameworks. + // It stores information that facilitates the host to process contents of the bundle without extraction. + // + // The location of deps.json and runtimeconfig.json is already available in the Bundle manifest. + // However, the data is cached here in order to simplify the bundle-processing performed by hostfxr. + struct header_fixed_v2_t + { + public: + location_t deps_json_location; + location_t runtimeconfig_json_location; + header_flags_t flags; + }; +#pragma pack(pop) + + struct header_t + { + public: + header_t(int32_t num_embedded_files = 0) + : m_num_embedded_files(num_embedded_files) + , m_bundle_id() + , m_v2_header(NULL) + + { + } + + static header_t read(reader_t& reader, bool need_exact_version); + const pal::string_t& bundle_id() { return m_bundle_id; } + int32_t num_embedded_files() { return m_num_embedded_files; } + + static const uint32_t major_version = 2; + static const uint32_t minor_version = 0; + + private: + int32_t m_num_embedded_files; + pal::string_t m_bundle_id; + const header_fixed_v2_t* m_v2_header; + + }; +} +#endif // __HEADER_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/manifest.cpp b/src/installer/corehost/cli/apphost/bundle/manifest.cpp new file mode 100644 index 00000000000..6de65d3a96b --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/manifest.cpp @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "manifest.h" + +using namespace bundle; + +manifest_t manifest_t::read(reader_t& reader, int32_t num_files) +{ + manifest_t manifest; + + for (int32_t i = 0; i < num_files; i++) + { + manifest.files.emplace_back(file_entry_t::read(reader)); + } + + return manifest; +} diff --git a/src/installer/corehost/cli/apphost/bundle/manifest.h b/src/installer/corehost/cli/apphost/bundle/manifest.h new file mode 100644 index 00000000000..ee8cd1edab5 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/manifest.h @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __MANIFEST_H__ +#define __MANIFEST_H__ + +#include <list> +#include "file_entry.h" + +namespace bundle +{ + // Bundle Manifest contains: + // Series of file entries (for each embedded file) + + class manifest_t + { + public: + std::vector<file_entry_t> files; + + static manifest_t read(reader_t &reader, int32_t num_files); + }; +} +#endif // __MANIFEST_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/marker.cpp b/src/installer/corehost/cli/apphost/bundle/marker.cpp new file mode 100644 index 00000000000..d2302923226 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/marker.cpp @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "marker.h" +#include "pal.h" +#include "trace.h" +#include "utils.h" + +using namespace bundle; + +int64_t marker_t::header_offset() +{ + // Contains the bundle_placeholder default value at compile time. + // If this is a single-file bundle, the last 8 bytes are replaced + // bundle-header replaced by "dotnet publish" with the offset + // where the bundle_header is located. + static volatile uint8_t placeholder[] = + { + // 8 bytes represent the bundle header-offset + // Zero for non-bundle apphosts (default). + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 64 bytes represent the bundle signature: SHA-256 for ".net core bundle" + 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, + 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, + 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, + 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae + }; + + volatile marker_t* marker = reinterpret_cast<volatile marker_t *>(placeholder); + + return marker->locator.bundle_header_offset; +} diff --git a/src/installer/corehost/cli/apphost/bundle/marker.h b/src/installer/corehost/cli/apphost/bundle/marker.h new file mode 100644 index 00000000000..52e185fbe79 --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/marker.h @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __MARKER_H__ +#define __MARKER_H__ + +#include <cstdint> + +namespace bundle +{ +#pragma pack(push, 1) + union marker_t + { + public: + uint8_t placeholder[40]; + struct + { + int64_t bundle_header_offset; + uint8_t signature[32]; + } locator; + + static int64_t header_offset(); + static bool is_bundle() + { + return header_offset() != 0; + } + }; +#pragma pack(pop) + +} +#endif // __MARKER_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/reader.cpp b/src/installer/corehost/cli/apphost/bundle/reader.cpp new file mode 100644 index 00000000000..e2fa9b6574c --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/reader.cpp @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "reader.h" +#include "error_codes.h" +#include "trace.h" + +using namespace bundle; + +const int8_t* reader_t::add_without_overflow(const int8_t* ptr, int64_t len) +{ + const int8_t* new_ptr = ptr + len; + + // The following check will fail in case len < 0 (which is also an error while reading) + // even if the actual arthmetic didn't overflow. + if (new_ptr < ptr) + { + trace::error(_X("Failure processing application bundle; possible file corruption.")); + trace::error(_X("Arithmetic overflow computing bundle-bounds.")); + throw StatusCode::BundleExtractionFailure; + } + + return new_ptr; +} + +void reader_t::set_offset(int64_t offset) +{ + if (offset < 0 || offset >= m_bound) + { + trace::error(_X("Failure processing application bundle; possible file corruption.")); + trace::error(_X("Arithmetic overflow while reading bundle.")); + throw StatusCode::BundleExtractionFailure; + } + + m_ptr = m_base_ptr + offset; +} + +void reader_t::bounds_check(int64_t len) +{ + const int8_t* post_read_ptr = add_without_overflow(m_ptr, len); + + // It is legal for post_read_ptr == m_bound_ptr after reading the last byte. + if (m_ptr < m_base_ptr || post_read_ptr > m_bound_ptr) + { + trace::error(_X("Failure processing application bundle; possible file corruption.")); + trace::error(_X("Bounds check failed while reading the bundle.")); + throw StatusCode::BundleExtractionFailure; + } +} + +// Handle the relatively uncommon scenario where the bundle ID or +// the relative-path of a file within the bundle is longer than 127 bytes +size_t reader_t::read_path_length() +{ + size_t length = 0; + + int8_t first_byte = read(); + + // If the high bit is set, it means there are more bytes to read. + if ((first_byte & 0x80) == 0) + { + length = first_byte; + } + else + { + int8_t second_byte = read(); + + if (second_byte & 0x80) + { + // There can be no more than two bytes in path_length + trace::error(_X("Failure processing application bundle; possible file corruption.")); + trace::error(_X("Path length encoding read beyond two bytes.")); + + throw StatusCode::BundleExtractionFailure; + } + + length = (second_byte << 7) | (first_byte & 0x7f); + } + + if (length <= 0 || length > PATH_MAX) + { + trace::error(_X("Failure processing application bundle; possible file corruption.")); + trace::error(_X("Path length is zero or too long.")); + throw StatusCode::BundleExtractionFailure; + } + + return length; +} + +void reader_t::read_path_string(pal::string_t &str) +{ + size_t size = read_path_length(); + std::unique_ptr<uint8_t[]> buffer{ new uint8_t[size + 1] }; + read(buffer.get(), size); + buffer[size] = 0; // null-terminator + pal::clr_palstring(reinterpret_cast<const char*>(buffer.get()), &str); +} diff --git a/src/installer/corehost/cli/apphost/bundle/reader.h b/src/installer/corehost/cli/apphost/bundle/reader.h new file mode 100644 index 00000000000..1824ece515a --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/reader.h @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __READER_H__ +#define __READER_H__ + +#include <cstdint> +#include "pal.h" + +namespace bundle +{ + // Helper class for reading sequentially from the memory-mapped bundle file. + struct reader_t + { + reader_t(const int8_t* base_ptr, int64_t bound) + : m_base_ptr(base_ptr) + , m_ptr(base_ptr) + , m_bound(bound) + , m_bound_ptr(add_without_overflow(base_ptr, bound)) + { + } + + public: + + void set_offset(int64_t offset); + + operator const int8_t*() const + { + return m_ptr; + } + + int8_t read() + { + bounds_check(); + return *m_ptr++; + } + + // Copy len bytes from m_ptr to dest + void read(void* dest, int64_t len) + { + bounds_check(len); + memcpy(dest, m_ptr, len); + m_ptr += len; + } + + // Return a pointer to the requested bytes within the memory-mapped file. + // Skip over len bytes. + const int8_t* read_direct(int64_t len) + { + bounds_check(len); + const int8_t *ptr = m_ptr; + m_ptr += len; + return ptr; + } + + size_t read_path_length(); + void read_path_string(pal::string_t &str); + + private: + + void bounds_check(int64_t len = 1); + static const int8_t* add_without_overflow(const int8_t* ptr, int64_t len); + + const int8_t* const m_base_ptr; + const int8_t* m_ptr; + const int64_t m_bound; + const int8_t* const m_bound_ptr; + }; +} + +#endif // __READER_H__ diff --git a/src/installer/corehost/cli/apphost/bundle/runner.cpp b/src/installer/corehost/cli/apphost/bundle/runner.cpp new file mode 100644 index 00000000000..50d74235d7a --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/runner.cpp @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include <memory> +#include "extractor.h" +#include "runner.h" +#include "trace.h" +#include "header.h" +#include "marker.h" +#include "manifest.h" + +using namespace bundle; + +void runner_t::map_host() +{ + m_bundle_map = (int8_t *) pal::map_file_readonly(m_bundle_path, m_bundle_length); + + if (m_bundle_map == nullptr) + { + trace::error(_X("Failure processing application bundle.")); + trace::error(_X("Couldn't memory map the bundle file for reading.")); + throw StatusCode::BundleExtractionIOError; + } +} + +void runner_t::unmap_host() +{ + if (!pal::unmap_file(m_bundle_map, m_bundle_length)) + { + trace::warning(_X("Failed to unmap bundle after extraction.")); + } +} + +// Current support for executing single-file bundles involves +// extraction of embedded files to actual files on disk. +// This method implements the file extraction functionality at startup. +StatusCode runner_t::extract() +{ + try + { + map_host(); + reader_t reader(m_bundle_map, m_bundle_length); + + // Read the bundle header + reader.set_offset(marker_t::header_offset()); + header_t header = header_t::read(reader, /* need_exact_version: */ true); + + // Read the bundle manifest + // Reader is at the correct offset + manifest_t manifest = manifest_t::read(reader, header.num_embedded_files()); + + // Extract the files + extractor_t extractor(header.bundle_id(), m_bundle_path, manifest); + m_extraction_dir = extractor.extract(reader); + + unmap_host(); + return StatusCode::Success; + } + catch (StatusCode e) + { + return e; + } +} + diff --git a/src/installer/corehost/cli/apphost/bundle/runner.h b/src/installer/corehost/cli/apphost/bundle/runner.h new file mode 100644 index 00000000000..07dadede0fd --- /dev/null +++ b/src/installer/corehost/cli/apphost/bundle/runner.h @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifndef __RUNNER_H__ +#define __RUNNER_H__ + +#include "error_codes.h" + +namespace bundle +{ + class runner_t + { + public: + runner_t(const pal::string_t& bundle_path) + : m_bundle_path(bundle_path) + , m_bundle_map(nullptr) + , m_bundle_length(0) + { + } + + StatusCode extract(); + + pal::string_t extraction_dir() + { + return m_extraction_dir; + } + + private: + void map_host(); + void unmap_host(); + + pal::string_t m_bundle_path; + pal::string_t m_extraction_dir; + int8_t* m_bundle_map; + size_t m_bundle_length; + }; +} + +#endif // __RUNNER_H__ |