| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <bootdata/decompress.h> |
| |
| #include <limits.h> |
| #include <string.h> |
| |
| #include <zircon/boot/bootdata.h> |
| #include <zircon/compiler.h> |
| #include <zircon/syscalls.h> |
| |
| #include <lz4/lz4.h> |
| |
| // The LZ4 Frame format is used to compress a bootfs image, but we cannot use |
| // the LZ4 library's decompression functions in userboot. The following |
| // definitions are used in the reimplementation of LZ4 Frame decompression, with |
| // a few restrictions on the frame options: |
| // - Blocks must be independent |
| // - No block checksums |
| // - Final content size must be included in frame header |
| // - Max block size is 64kB |
| // |
| // See https://github.com/lz4/lz4/blob/dev/lz4_Frame_format.md for details. |
| #define ZX_LZ4_MAGIC 0x184D2204 |
| #define ZX_LZ4_VERSION (1 << 6) |
| |
| typedef struct { |
| uint8_t flag; |
| uint8_t block_desc; |
| uint64_t content_size; |
| uint8_t header_cksum; |
| } __PACKED lz4_frame_desc; |
| |
| #define ZX_LZ4_FLAG_VERSION (1 << 6) |
| #define ZX_LZ4_FLAG_BLOCK_DEP (1 << 5) |
| #define ZX_LZ4_FLAG_BLOCK_CKSUM (1 << 4) |
| #define ZX_LZ4_FLAG_CONTENT_SZ (1 << 3) |
| #define ZX_LZ4_FLAG_CONTENT_CKSUM (1 << 2) |
| #define ZX_LZ4_FLAG_RESERVED 0x03 |
| |
| #define ZX_LZ4_BLOCK_MAX_MASK (7 << 4) |
| #define ZX_LZ4_BLOCK_64KB (4 << 4) |
| #define ZX_LZ4_BLOCK_256KB (5 << 4) |
| #define ZX_LZ4_BLOCK_1MB (6 << 4) |
| #define ZX_LZ4_BLOCK_4MB (7 << 4) |
| |
| static zx_status_t check_lz4_frame(const lz4_frame_desc* fd, |
| size_t expected, const char** err) { |
| if ((fd->flag & ZX_LZ4_FLAG_VERSION) != ZX_LZ4_VERSION) { |
| *err = "bad lz4 version for bootfs"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if ((fd->flag & ZX_LZ4_FLAG_BLOCK_DEP) == 0) { |
| *err = "bad lz4 flag (blocks must be independent)"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (fd->flag & ZX_LZ4_FLAG_BLOCK_CKSUM) { |
| *err = "bad lz4 flag (block checksum must be disabled)"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if ((fd->flag & ZX_LZ4_FLAG_CONTENT_SZ) == 0) { |
| *err = "bad lz4 flag (content size must be included)"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (fd->flag & ZX_LZ4_FLAG_RESERVED) { |
| *err = "bad lz4 flag (reserved bits in flg must be zero)"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if ((fd->block_desc & ZX_LZ4_BLOCK_MAX_MASK) != ZX_LZ4_BLOCK_64KB) { |
| *err = "bad lz4 flag (max block size must be 64k)"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (fd->block_desc & ~ZX_LZ4_BLOCK_MAX_MASK) { |
| *err = "bad lz4 flag (reserved bits in bd must be zero)"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (fd->content_size != expected) { |
| *err = "lz4 content size does not match bootdata outsize"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| // TODO: header checksum |
| return ZX_OK; |
| } |
| |
| static zx_status_t decompress_bootfs_vmo(zx_handle_t vmar, const uint8_t* data, |
| size_t _outsize, zx_handle_t* out, |
| const char** err) { |
| if (*(const uint32_t*)data != ZX_LZ4_MAGIC) { |
| *err = "bad magic number for compressed bootfs"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| data += sizeof(uint32_t); |
| |
| zx_status_t status = check_lz4_frame((const lz4_frame_desc*)data, _outsize, err); |
| if (status < 0) |
| return status; |
| data += sizeof(lz4_frame_desc); |
| |
| size_t outsize = (_outsize + 4095) & ~4095; |
| if (outsize < _outsize) { |
| // newsize wrapped, which means the outsize was too large |
| *err = "lz4 output size too large"; |
| return ZX_ERR_NO_MEMORY; |
| } |
| zx_handle_t dst_vmo; |
| status = zx_vmo_create((uint64_t)outsize, 0, &dst_vmo); |
| if (status < 0) { |
| *err = "zx_vmo_create failed for decompressing bootfs"; |
| return status; |
| } |
| zx_object_set_property(dst_vmo, ZX_PROP_NAME, "bootfs", 6); |
| |
| uintptr_t dst_addr = 0; |
| status = zx_vmar_map(vmar, |
| ZX_VM_PERM_READ|ZX_VM_PERM_WRITE, |
| 0, dst_vmo, 0, outsize, &dst_addr); |
| if (status < 0) { |
| *err = "zx_vmar_map failed on bootfs vmo during decompression"; |
| return status; |
| } |
| |
| size_t remaining = outsize; |
| uint8_t* dst = (uint8_t*)dst_addr; |
| |
| // Read each LZ4 block and decompress it. Block sizes are 32 bits. |
| uint32_t blocksize = *(const uint32_t*)data; |
| data += sizeof(uint32_t); |
| while (blocksize) { |
| // If the data is uncompressed, the high bit is 1. |
| if (blocksize >> 31) { |
| uint32_t actual = blocksize & 0x7fffffff; |
| memcpy(dst, data, actual); |
| dst += actual; |
| data += actual; |
| if (remaining - actual > remaining) { |
| // Remaining wrapped around (would be negative if signed) |
| *err = "bootdata outsize too small for lz4 decompression"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| remaining -= actual; |
| } else { |
| int dcmp = LZ4_decompress_safe((const char*)data, (char*)dst, blocksize, remaining); |
| if (dcmp < 0) { |
| *err = "lz4 decompression failed"; |
| return ZX_ERR_BAD_STATE; |
| } |
| dst += dcmp; |
| data += blocksize; |
| if (remaining - dcmp > remaining) { |
| // Remaining wrapped around (would be negative if signed) |
| *err = "bootdata outsize too small for lz4 decompression"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| remaining -= dcmp; |
| } |
| |
| blocksize = *(uint32_t*)data; |
| data += sizeof(uint32_t); |
| } |
| |
| // Sanity check: verify that we didn't have more than one page leftover. |
| // The bootdata header should have specified the exact outsize needed, which |
| // we rounded up to the next full page. |
| if (remaining > 4095) { |
| *err = "bootdata size error; outsize does not match decompressed size"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| status = zx_vmar_unmap(vmar, dst_addr, outsize); |
| if (status < 0) { |
| *err = "zx_vmar_unmap after decompress failed"; |
| return status; |
| } |
| *out = dst_vmo; |
| return ZX_OK; |
| } |
| |
| zx_status_t decompress_bootdata(zx_handle_t vmar, zx_handle_t vmo, |
| size_t offset, size_t length, |
| zx_handle_t* out, const char** err) { |
| *err = "none"; |
| |
| if (length > SIZE_MAX) { |
| *err = "bootfs VMO too large to map"; |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| |
| uintptr_t addr = 0; |
| size_t aligned_offset = offset & ~(PAGE_SIZE - 1); |
| size_t align_shift = offset - aligned_offset; |
| length += align_shift; |
| zx_status_t status = zx_vmar_map(vmar, ZX_VM_PERM_READ, 0, vmo, aligned_offset, length, &addr); |
| if (status < 0) { |
| *err = "zx_vmar_map failed on bootfs vmo"; |
| return status; |
| } |
| uintptr_t bootdata_addr = addr + align_shift; |
| |
| const bootdata_t* hdr = (bootdata_t*)bootdata_addr; |
| bootdata_addr += sizeof(bootdata_t); |
| |
| switch (hdr->type) { |
| case BOOTDATA_BOOTFS_BOOT: |
| case BOOTDATA_BOOTFS_SYSTEM: |
| case BOOTDATA_RAMDISK: |
| if (hdr->flags & BOOTDATA_BOOTFS_FLAG_COMPRESSED) { |
| status = decompress_bootfs_vmo(vmar, (const uint8_t*)bootdata_addr, hdr->extra, out, err); |
| } |
| break; |
| default: |
| *err = "unknown bootdata type, not attempting decompression\n"; |
| status = ZX_ERR_NOT_SUPPORTED; |
| break; |
| } |
| |
| zx_status_t s = zx_vmar_unmap(vmar, addr, length); |
| if (s < 0) { |
| *err = "zx_vmar_unmap failed on bootfs vmo"; |
| return s; |
| } |
| |
| return status; |
| } |