diff options
Diffstat (limited to 'mozglue/linker/Mappable.cpp')
-rw-r--r-- | mozglue/linker/Mappable.cpp | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/mozglue/linker/Mappable.cpp b/mozglue/linker/Mappable.cpp new file mode 100644 index 0000000000..cacd6a46f6 --- /dev/null +++ b/mozglue/linker/Mappable.cpp @@ -0,0 +1,376 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <fcntl.h> +#include <unistd.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <cstring> +#include <cstdlib> +#include <cstdio> +#include <string> + +#include "Mappable.h" + +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/UniquePtr.h" + +#ifdef ANDROID +# include "mozilla/Ashmem.h" +#endif +#include <sys/stat.h> +#include <errno.h> +#include "ElfLoader.h" +#include "XZStream.h" +#include "Logging.h" + +using mozilla::MakeUnique; +using mozilla::UniquePtr; + +class CacheValidator { + public: + CacheValidator(const char* aCachedLibPath, Zip* aZip, Zip::Stream* aStream) + : mCachedLibPath(aCachedLibPath) { + static const char kChecksumSuffix[] = ".crc"; + + mCachedChecksumPath = + MakeUnique<char[]>(strlen(aCachedLibPath) + sizeof(kChecksumSuffix)); + sprintf(mCachedChecksumPath.get(), "%s%s", aCachedLibPath, kChecksumSuffix); + DEBUG_LOG("mCachedChecksumPath: %s", mCachedChecksumPath.get()); + + mChecksum = aStream->GetCRC32(); + DEBUG_LOG("mChecksum: %x", mChecksum); + } + + // Returns whether the cache is valid and up-to-date. + bool IsValid() const { + // Validate based on checksum. + RefPtr<Mappable> checksumMap = + MappableFile::Create(mCachedChecksumPath.get()); + if (!checksumMap) { + // Force caching if checksum is missing in cache. + return false; + } + + DEBUG_LOG("Comparing %x with %s", mChecksum, mCachedChecksumPath.get()); + MappedPtr checksumBuf = checksumMap->mmap(nullptr, checksumMap->GetLength(), + PROT_READ, MAP_PRIVATE, 0); + if (checksumBuf == MAP_FAILED) { + WARN("Couldn't map %s to validate checksum", mCachedChecksumPath.get()); + return false; + } + if (memcmp(checksumBuf, &mChecksum, sizeof(mChecksum))) { + return false; + } + return !access(mCachedLibPath.c_str(), R_OK); + } + + // Caches the APK-provided checksum used in future cache validations. + void CacheChecksum() const { + AutoCloseFD fd(open(mCachedChecksumPath.get(), + O_TRUNC | O_RDWR | O_CREAT | O_NOATIME, + S_IRUSR | S_IWUSR)); + if (fd == -1) { + WARN("Couldn't open %s to update checksum", mCachedChecksumPath.get()); + return; + } + + DEBUG_LOG("Updating checksum %s", mCachedChecksumPath.get()); + + const size_t size = sizeof(mChecksum); + size_t written = 0; + while (written < size) { + ssize_t ret = + write(fd, reinterpret_cast<const uint8_t*>(&mChecksum) + written, + size - written); + if (ret >= 0) { + written += ret; + } else if (errno != EINTR) { + WARN("Writing checksum %s failed with errno %d", + mCachedChecksumPath.get(), errno); + break; + } + } + } + + private: + const std::string mCachedLibPath; + UniquePtr<char[]> mCachedChecksumPath; + uint32_t mChecksum; +}; + +Mappable* MappableFile::Create(const char* path) { + int fd = open(path, O_RDONLY); + if (fd != -1) return new MappableFile(fd); + return nullptr; +} + +MemoryRange MappableFile::mmap(const void* addr, size_t length, int prot, + int flags, off_t offset) { + MOZ_ASSERT(fd != -1); + MOZ_ASSERT(!(flags & MAP_SHARED)); + flags |= MAP_PRIVATE; + + return MemoryRange::mmap(const_cast<void*>(addr), length, prot, flags, fd, + offset); +} + +void MappableFile::finalize() { + /* Close file ; equivalent to close(fd.forget()) */ + fd = -1; +} + +size_t MappableFile::GetLength() const { + struct stat st; + return fstat(fd, &st) ? 0 : st.st_size; +} + +Mappable* MappableExtractFile::Create(const char* name, Zip* zip, + Zip::Stream* stream) { + MOZ_ASSERT(zip && stream); + + const char* cachePath = getenv("MOZ_LINKER_CACHE"); + if (!cachePath || !*cachePath) { + WARN( + "MOZ_LINKER_EXTRACT is set, but not MOZ_LINKER_CACHE; " + "not extracting"); + return nullptr; + } + + // Ensure that the cache dir is private. + chmod(cachePath, 0770); + + UniquePtr<char[]> path = + MakeUnique<char[]>(strlen(cachePath) + strlen(name) + 2); + sprintf(path.get(), "%s/%s", cachePath, name); + + CacheValidator validator(path.get(), zip, stream); + if (validator.IsValid()) { + DEBUG_LOG("Reusing %s", static_cast<char*>(path.get())); + return MappableFile::Create(path.get()); + } + DEBUG_LOG("Extracting to %s", static_cast<char*>(path.get())); + AutoCloseFD fd; + fd = open(path.get(), O_TRUNC | O_RDWR | O_CREAT | O_NOATIME, + S_IRUSR | S_IWUSR); + if (fd == -1) { + ERROR("Couldn't open %s to decompress library", path.get()); + return nullptr; + } + AutoUnlinkFile file(path.release()); + if (stream->GetType() == Zip::Stream::DEFLATE) { + if (ftruncate(fd, stream->GetUncompressedSize()) == -1) { + ERROR("Couldn't ftruncate %s to decompress library", file.get()); + return nullptr; + } + /* Map the temporary file for use as inflate buffer */ + MappedPtr buffer(MemoryRange::mmap(nullptr, stream->GetUncompressedSize(), + PROT_WRITE, MAP_SHARED, fd, 0)); + if (buffer == MAP_FAILED) { + ERROR("Couldn't map %s to decompress library", file.get()); + return nullptr; + } + + z_stream zStream = stream->GetZStream(buffer); + + /* Decompress */ + if (inflateInit2(&zStream, -MAX_WBITS) != Z_OK) { + ERROR("inflateInit failed: %s", zStream.msg); + return nullptr; + } + if (inflate(&zStream, Z_FINISH) != Z_STREAM_END) { + ERROR("inflate failed: %s", zStream.msg); + return nullptr; + } + if (inflateEnd(&zStream) != Z_OK) { + ERROR("inflateEnd failed: %s", zStream.msg); + return nullptr; + } + if (zStream.total_out != stream->GetUncompressedSize()) { + ERROR("File not fully uncompressed! %ld / %d", zStream.total_out, + static_cast<unsigned int>(stream->GetUncompressedSize())); + return nullptr; + } + } else if (XZStream::IsXZ(stream->GetBuffer(), stream->GetSize())) { + XZStream xzStream(stream->GetBuffer(), stream->GetSize()); + + if (!xzStream.Init()) { + ERROR("Couldn't initialize XZ decoder"); + return nullptr; + } + DEBUG_LOG("XZStream created, compressed=%" PRIuPTR + ", uncompressed=%" PRIuPTR, + xzStream.Size(), xzStream.UncompressedSize()); + + if (ftruncate(fd, xzStream.UncompressedSize()) == -1) { + ERROR("Couldn't ftruncate %s to decompress library", file.get()); + return nullptr; + } + MappedPtr buffer(MemoryRange::mmap(nullptr, xzStream.UncompressedSize(), + PROT_WRITE, MAP_SHARED, fd, 0)); + if (buffer == MAP_FAILED) { + ERROR("Couldn't map %s to decompress library", file.get()); + return nullptr; + } + const size_t written = xzStream.Decode(buffer, buffer.GetLength()); + DEBUG_LOG("XZStream decoded %" PRIuPTR, written); + if (written != buffer.GetLength()) { + ERROR("Error decoding XZ file %s", file.get()); + return nullptr; + } + } else { + return nullptr; + } + + validator.CacheChecksum(); + return new MappableExtractFile(fd.forget(), file.release()); +} + +/** + * _MappableBuffer is a buffer which content can be mapped at different + * locations in the virtual address space. + * On Linux, uses a (deleted) temporary file on a tmpfs for sharable content. + * On Android, uses ashmem. + */ +class _MappableBuffer : public MappedPtr { + public: + /** + * Returns a _MappableBuffer instance with the given name and the given + * length. + */ + static _MappableBuffer* Create(const char* name, size_t length) { + AutoCloseFD fd; + const char* ident; +#ifdef ANDROID + /* On Android, initialize an ashmem region with the given length */ + fd = mozilla::android::ashmem_create(name, length); + ident = name; +#else + /* On Linux, use /dev/shm as base directory for temporary files, assuming + * it's on tmpfs */ + /* TODO: check that /dev/shm is tmpfs */ + char path[256]; + sprintf(path, "/dev/shm/%s.XXXXXX", name); + fd = mkstemp(path); + if (fd == -1) return nullptr; + unlink(path); + ftruncate(fd, length); + ident = path; +#endif + + void* buf = + ::mmap(nullptr, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (buf != MAP_FAILED) { + DEBUG_LOG("Decompression buffer of size 0x%" PRIxPTR + " in " +#ifdef ANDROID + "ashmem " +#endif + "\"%s\", mapped @%p", + length, ident, buf); + return new _MappableBuffer(fd.forget(), buf, length); + } + return nullptr; + } + + void* mmap(const void* addr, size_t length, int prot, int flags, + off_t offset) { + MOZ_ASSERT(fd != -1); +#ifdef ANDROID + /* Mapping ashmem MAP_PRIVATE is like mapping anonymous memory, even when + * there is content in the ashmem */ + if (flags & MAP_PRIVATE) { + flags &= ~MAP_PRIVATE; + flags |= MAP_SHARED; + } +#endif + return ::mmap(const_cast<void*>(addr), length, prot, flags, fd, offset); + } + + private: + _MappableBuffer(int fd, void* buf, size_t length) + : MappedPtr(buf, length), fd(fd) {} + + /* File descriptor for the temporary file or ashmem */ + AutoCloseFD fd; +}; + +Mappable* MappableDeflate::Create(const char* name, Zip* zip, + Zip::Stream* stream) { + MOZ_ASSERT(stream->GetType() == Zip::Stream::DEFLATE); + _MappableBuffer* buf = + _MappableBuffer::Create(name, stream->GetUncompressedSize()); + if (buf) return new MappableDeflate(buf, zip, stream); + return nullptr; +} + +MappableDeflate::MappableDeflate(_MappableBuffer* buf, Zip* zip, + Zip::Stream* stream) + : zip(zip), buffer(buf), zStream(stream->GetZStream(*buf)) {} + +MappableDeflate::~MappableDeflate() {} + +MemoryRange MappableDeflate::mmap(const void* addr, size_t length, int prot, + int flags, off_t offset) { + MOZ_ASSERT(buffer); + MOZ_ASSERT(!(flags & MAP_SHARED)); + flags |= MAP_PRIVATE; + + /* The deflate stream is uncompressed up to the required offset + length, if + * it hasn't previously been uncompressed */ + ssize_t missing = offset + length + zStream.avail_out - buffer->GetLength(); + if (missing > 0) { + uInt avail_out = zStream.avail_out; + zStream.avail_out = missing; + if ((*buffer == zStream.next_out) && + (inflateInit2(&zStream, -MAX_WBITS) != Z_OK)) { + ERROR("inflateInit failed: %s", zStream.msg); + return MemoryRange(MAP_FAILED, 0); + } + int ret = inflate(&zStream, Z_SYNC_FLUSH); + if (ret < 0) { + ERROR("inflate failed: %s", zStream.msg); + return MemoryRange(MAP_FAILED, 0); + } + if (ret == Z_NEED_DICT) { + ERROR("zstream requires a dictionary. %s", zStream.msg); + return MemoryRange(MAP_FAILED, 0); + } + zStream.avail_out = avail_out - missing + zStream.avail_out; + if (ret == Z_STREAM_END) { + if (inflateEnd(&zStream) != Z_OK) { + ERROR("inflateEnd failed: %s", zStream.msg); + return MemoryRange(MAP_FAILED, 0); + } + if (zStream.total_out != buffer->GetLength()) { + ERROR("File not fully uncompressed! %ld / %d", zStream.total_out, + static_cast<unsigned int>(buffer->GetLength())); + return MemoryRange(MAP_FAILED, 0); + } + } + } +#if defined(ANDROID) && defined(__arm__) + if (prot & PROT_EXEC) { + /* We just extracted data that may be executed in the future. + * We thus need to ensure Instruction and Data cache coherency. */ + DEBUG_LOG("cacheflush(%p, %p)", *buffer + offset, + *buffer + (offset + length)); + cacheflush(reinterpret_cast<uintptr_t>(*buffer + offset), + reinterpret_cast<uintptr_t>(*buffer + (offset + length)), 0); + } +#endif + + return MemoryRange(buffer->mmap(addr, length, prot, flags, offset), length); +} + +void MappableDeflate::finalize() { + /* Free zlib internal buffers */ + inflateEnd(&zStream); + /* Free decompression buffer */ + buffer = nullptr; + /* Remove reference to Zip archive */ + zip = nullptr; +} + +size_t MappableDeflate::GetLength() const { return buffer->GetLength(); } |