/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "dump.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "linux/incrementalfs.h" #include #include #include #include #include #include using namespace std::literals; namespace { // stuff from the internal incfs implementation #ifndef __packed #define __packed __attribute__((packed)) #endif struct mem_range { char* data; size_t len; }; #define INCFS_MAX_NAME_LEN 255 #define INCFS_FORMAT_V1 1 #define INCFS_FORMAT_CURRENT_VER INCFS_FORMAT_V1 enum incfs_metadata_type { INCFS_MD_NONE = 0, INCFS_MD_BLOCK_MAP = 1, INCFS_MD_FILE_ATTR = 2, INCFS_MD_SIGNATURE = 3, INCFS_MD_STATUS = 4, INCFS_MD_VERITY_SIGNATURE = 5, }; enum incfs_file_header_flags { INCFS_FILE_MAPPED = 1 << 1, }; /* Header included at the beginning of all metadata records on the disk. */ struct incfs_md_header { uint8_t h_md_entry_type; /* * Size of the metadata record. * (e.g. inode, dir entry etc) not just this struct. */ int16_t h_record_size; /* * CRC32 of the metadata record. * (e.g. inode, dir entry etc) not just this struct. */ int32_t h_unused1; /* Offset of the next metadata entry if any */ int64_t h_next_md_offset; /* Offset of the previous metadata entry if any */ int64_t h_unused2; } __packed; /* Backing file header */ struct incfs_file_header { /* Magic number: INCFS_MAGIC_NUMBER */ __le64 fh_magic; /* Format version: INCFS_FORMAT_CURRENT_VER */ __le64 fh_version; /* sizeof(incfs_file_header) */ __le16 fh_header_size; /* INCFS_DATA_FILE_BLOCK_SIZE */ __le16 fh_data_block_size; /* File flags, from incfs_file_header_flags */ __le32 fh_flags; union { /* Standard incfs file */ struct { /* Offset of the first metadata record */ __le64 fh_first_md_offset; /* Full size of the file's content */ __le64 fh_file_size; /* File uuid */ incfs_uuid_t fh_uuid; }; /* Mapped file - INCFS_FILE_MAPPED set in fh_flags */ struct { /* Offset in original file */ __le64 fh_original_offset; /* Full size of the file's content */ __le64 fh_mapped_file_size; /* Original file's uuid */ incfs_uuid_t fh_original_uuid; }; }; } __packed; enum incfs_block_map_entry_flags { INCFS_BLOCK_COMPRESSED_LZ4 = 1, INCFS_BLOCK_COMPRESSED_ZSTD = 2, /* Reserve 3 bits for compression alg */ INCFS_BLOCK_COMPRESSED_MASK = 7, }; /* Block map entry pointing to an actual location of the data block. */ struct incfs_blockmap_entry { /* Offset of the actual data block. Lower 32 bits */ int32_t me_data_offset_lo; /* Offset of the actual data block. Higher 16 bits */ int16_t me_data_offset_hi; /* How many bytes the data actually occupies in the backing file */ int16_t me_data_size; /* Block flags from incfs_block_map_entry_flags */ int16_t me_flags; } __packed; /* Metadata record for locations of file blocks. Type = INCFS_MD_BLOCK_MAP */ struct incfs_blockmap { struct incfs_md_header m_header; /* Base offset of the array of incfs_blockmap_entry */ int64_t m_base_offset; /* Size of the map entry array in blocks */ int32_t m_block_count; } __packed; /* Metadata record for file signature. Type = INCFS_MD_SIGNATURE */ struct incfs_file_signature { struct incfs_md_header sg_header; int32_t sg_sig_size; /* The size of the signature. */ int64_t sg_sig_offset; /* Signature's offset in the backing file */ int32_t sg_hash_tree_size; /* The size of the hash tree. */ int64_t sg_hash_tree_offset; /* Hash tree offset in the backing file */ } __packed; struct incfs_status { struct incfs_md_header is_header; __le32 is_data_blocks_written; /* Number of data blocks written */ __le32 is_hash_blocks_written; /* Number of hash blocks written */ __le32 is_dummy[6]; /* Spare fields */ } __packed; /* * Metadata record for verity signature. Type = INCFS_MD_VERITY_SIGNATURE * * This record will only exist for verity-enabled files with signatures. Verity * enabled files without signatures do not have this record. This signature is * checked by fs-verity identically to any other fs-verity signature. */ struct incfs_file_verity_signature { struct incfs_md_header vs_header; /* The size of the signature */ __le32 vs_size; /* Signature's offset in the backing file */ __le64 vs_offset; } __packed; typedef union { struct incfs_md_header md_header; struct incfs_blockmap blockmap; struct incfs_file_signature signature; struct incfs_status status; struct incfs_file_verity_signature verity_signature; } md_buffer; #define INCFS_MAX_METADATA_RECORD_SIZE sizeof(md_buffer) class Dump { public: Dump(std::string_view backingFile) : mBackingFile(android::base::Basename(std::string(backingFile))), mIn(backingFile) {} void run() { if (!mIn) { err() << "bad input file name " << mBackingFile; return; } auto header = read(); out() << "header: " << hex(header.fh_magic) << ", " << header.fh_version << ", " << hex(header.fh_data_block_size) << ", " << header.fh_header_size << ", " << header.fh_file_size; if (header.fh_magic != INCFS_MAGIC_NUMBER) { err() << "bad magic, expected: " << hex(INCFS_MAGIC_NUMBER); } if (header.fh_version != INCFS_FORMAT_CURRENT_VER) { err() << "bad version, expected: " << INCFS_FORMAT_CURRENT_VER; } if (header.fh_data_block_size != INCFS_DATA_FILE_BLOCK_SIZE) { err() << "bad data block size, expected: " << hex(INCFS_DATA_FILE_BLOCK_SIZE); } if (header.fh_header_size != sizeof(header)) { err() << "bad header size, expected: " << sizeof(header); } { auto ostream = out() << "flags: " << hex(header.fh_flags); if (header.fh_flags & INCFS_FILE_MAPPED) { ostream << " (mapped file)"; } } if (header.fh_flags & INCFS_FILE_MAPPED) { out() << "source " << toString(header.fh_original_uuid); out() << "size " << header.fh_mapped_file_size << " @ " << hex(header.fh_original_offset); } else { out() << "uuid " << toString(header.fh_uuid); out() << "size " << header.fh_file_size; out() << "first md offset " << hex(header.fh_first_md_offset); int64_t metadataOffset = header.fh_first_md_offset; if (metadataOffset >= mIn.tellg()) { if (metadataOffset > mIn.tellg()) { out() << "gap of " << metadataOffset - mIn.tellg() << " bytes to the first metadata record"; } incfs_md_header prevMd = {}; do { dumpMd(metadataOffset, prevMd); } while (metadataOffset != 0); } } out() << "finished" << (mIn ? "" : " with read errors"); } private: auto scopedNesting() { ++mNesting; auto undoNesting = [this](auto) { --mNesting; }; return std::unique_ptr(this, std::move(undoNesting)); } const char* mdType(int type) { switch (type) { case INCFS_MD_NONE: return "none"; case INCFS_MD_BLOCK_MAP: return "block map"; case INCFS_MD_STATUS: return "status"; case INCFS_MD_SIGNATURE: return "signature"; case INCFS_MD_VERITY_SIGNATURE: return "verity signature"; default: return "unknown"; } } std::string blockFlags(int flags) { if (!flags) { return {}; } std::string res = "("; auto compression = flags & INCFS_BLOCK_COMPRESSED_MASK; if (compression == INCFS_BLOCK_COMPRESSED_LZ4) { res += "|lz4-compressed|"; } else if (flags == INCFS_BLOCK_COMPRESSED_ZSTD) { res += "|zstd-compressed|"; } res += ")"; return res; } void dumpBlockmap(int64_t offset, int64_t count) { auto nesting = scopedNesting(); mIn.seekg(offset); for (int64_t i = 0; i != count; ++i) { auto ostream = out() << i << " @ " << hex(mIn.tellg()) << ": [ "; auto block = read(); auto blockOffset = uint64_t(block.me_data_offset_lo) | (uint64_t(block.me_data_offset_hi) << 32); if (blockOffset) { ostream << block.me_data_size << " @ " << hex(blockOffset); } else { ostream << "missing"; } ostream << " ], flags = " << block.me_flags << blockFlags(block.me_flags); } } void dumpTree(int64_t offset, int64_t size) { auto nesting = scopedNesting(); out() << "tree " << offset << " " << size; } void dumpMd(int64_t& offset, incfs_md_header& prevMd) { md_buffer mdBuf = {}; auto& md = mdBuf.md_header; md = readAt(offset); out() << "metadata: " << mdType(md.h_md_entry_type) << "(" << int(md.h_md_entry_type) << ")"; auto nesting = scopedNesting(); out() << "record size: " << md.h_record_size; out() << "next md offset: " << hex(md.h_next_md_offset); { switch (md.h_md_entry_type) { case INCFS_MD_NONE: out() << "nothing here"; break; case INCFS_MD_BLOCK_MAP: { auto& bm = mdBuf.blockmap; bm = readAt(offset); out() << "offset: " << hex(bm.m_base_offset); out() << "block count: " << bm.m_block_count; dumpBlockmap(bm.m_base_offset, bm.m_block_count); break; } case INCFS_MD_SIGNATURE: { auto& sig = mdBuf.signature; sig = readAt(offset); out() << "signature size: " << sig.sg_sig_size; out() << "signature offset: " << hex(sig.sg_sig_offset); out() << "hash tree size: " << sig.sg_hash_tree_size; out() << "hash tree offset: " << hex(sig.sg_hash_tree_offset); dumpTree(sig.sg_hash_tree_offset, sig.sg_hash_tree_size); break; } case INCFS_MD_STATUS: { auto& st = mdBuf.status; st = readAt(offset); out() << "data blocks written: " << st.is_data_blocks_written; out() << "hash blocks written: " << st.is_hash_blocks_written; break; } case INCFS_MD_VERITY_SIGNATURE: { auto& vs = mdBuf.verity_signature; vs = readAt(offset); out() << "verity signature size: " << vs.vs_size; out() << "verity signature offset: " << hex(vs.vs_offset); break; } default: out() << "don't know how to handle it"; break; } } updateMaxPos(); prevMd = md; offset = md.h_next_md_offset; } struct OstreamWrapper { explicit OstreamWrapper(std::ostream& wrapped) : mWrapped(&wrapped) {} OstreamWrapper(OstreamWrapper&& other) noexcept : mWrapped(std::exchange(other.mWrapped, nullptr)) {} ~OstreamWrapper() { if (mWrapped) { *mWrapped << '\n'; } } template OstreamWrapper& operator<<(const T& t) & { *mWrapped << t; return *this; } template OstreamWrapper&& operator<<(const T& t) && { *this << t; return std::move(*this); } private: std::ostream* mWrapped; }; static std::string hex(uint64_t t) { char buf[32] = {}; snprintf(buf, std::size(buf) - 1, "0x%llx", (unsigned long long)t); return buf; } static std::string toString(incfs_uuid_t uuid) { std::string res; for (unsigned char b : uuid.bytes) { char buf[3] = {}; snprintf(buf, std::size(buf) - 1, "%02x", (unsigned int)b); res += buf; } return res; } OstreamWrapper out() const { nesting(std::cout); std::cout << "[" << mBackingFile << "] "; return OstreamWrapper(std::cout); } OstreamWrapper err() const { nesting(std::cerr); std::cerr << "[" << mBackingFile << "] "; return OstreamWrapper(std::cerr); } void nesting(std::ostream& out) const { for (int i = 0; i < mNesting; ++i) { out << " "; } } template std::remove_reference_t read() { std::remove_reference_t res; mIn.read((char*)&res, sizeof(res)); return res; } template std::remove_reference_t readAt(int64_t pos) { mIn.seekg(pos); return read(); } void skip(int64_t count) { mIn.seekg(count, std::ios_base::cur); } void updateMaxPos() { mMaxDumpedPos = std::max(mMaxDumpedPos, mIn.tellg()); } std::string mBackingFile; std::ifstream mIn; int mNesting = 0; int64_t mMaxDumpedPos = 0; }; } // namespace namespace android::incfs { void dump(std::string_view backingFile) { Dump(backingFile).run(); } } // namespace android::incfs