// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2017-2018 HUAWEI, Inc. * https://www.huawei.com/ * Copyright (C) 2021, Alibaba Cloud */ #include #include #include #include #include #include #include #include #include #include "internal.h" /* * For flat inline files, compute the "inline tail" start offset. * Linux EROFS semantics: the last logical block may be tailpacked * into the inode metadata area. */ static uint64_t erofs_inline_tail_start(const struct erofs_mount *em, const struct erofs_node *en) { if (en->size == 0) return (0); return (roundup2(en->size, (uint64_t)em->block_size) - em->block_size); } static int erofs_check_device_range(const struct erofs_mount *em, const struct erofs_device_info *dif, uint64_t off, uint64_t len) { uint64_t end, limit; if (dif->blocks > (UINT64_MAX >> em->block_bits) || __builtin_add_overflow(off, len, &end)) return (EINTEGRITY); limit = dif->blocks << em->block_bits; return (end > limit ? EINTEGRITY : 0); } int erofs_map_dev(struct erofs_mount *em, struct erofs_map_dev *map) { struct erofs_device_info *dif; uint64_t start; unsigned int id; int error; map->m_em = em; map->m_dif = &em->dif0; if (map->m_deviceid != 0) { if (map->m_deviceid > em->extra_devices || em->devs == NULL) return (ENODEV); dif = &em->devs[map->m_deviceid - 1]; error = erofs_check_device_range(em, dif, map->m_pa, map->m_plen); if (error != 0) return (error); if (em->flatdev) { if (dif->uniaddr > (UINT64_MAX >> em->block_bits)) return (EINTEGRITY); start = dif->uniaddr << em->block_bits; if (__builtin_add_overflow(map->m_pa, start, &map->m_pa)) return (EINTEGRITY); return (0); } if (dif->devvp == NULL || dif->cp == NULL) return (ENODEV); map->m_dif = dif; return (0); } if (em->extra_devices == 0) return (0); if (em->flatdev) { error = erofs_check_device_range(em, &em->dif0, map->m_pa, map->m_plen); if (error == 0) return (0); for (id = 0; id < em->extra_devices; ++id) { dif = &em->devs[id]; if (dif->uniaddr == 0 || dif->uniaddr > (UINT64_MAX >> em->block_bits)) continue; start = dif->uniaddr << em->block_bits; if (map->m_pa < start) continue; error = erofs_check_device_range(em, dif, map->m_pa - start, map->m_plen); if (error == 0) return (0); if (map->m_pa - start < (dif->blocks << em->block_bits)) return (error); } return (EINTEGRITY); } for (id = 0; id < em->extra_devices; ++id) { dif = &em->devs[id]; if (dif->uniaddr == 0) continue; if (dif->uniaddr > (UINT64_MAX >> em->block_bits)) return (EINTEGRITY); start = dif->uniaddr << em->block_bits; if (map->m_pa >= start && map->m_pa - start < (dif->blocks << em->block_bits)) { error = erofs_check_device_range(em, dif, map->m_pa - start, map->m_plen); if (error != 0) return (error); if (dif->devvp == NULL || dif->cp == NULL) return (ENODEV); map->m_pa -= start; map->m_dif = dif; break; } } return (0); } /* Map chunk-based file to physical extent */ static int erofs_map_blocks_chunk(struct erofs_mount *em, struct erofs_node *en, uint64_t loff, uint64_t *phys_off, unsigned int *device_id, size_t *run_len, bool *hole) { struct erofs_inode_chunk_index *idx; void *buf; uint64_t chunk_idx, idx_off, chunk_size, entry_size, chunk_off; uint64_t idx_base, image_size, addrmask; uint64_t blkaddr; uint16_t raw_device_id; int error; chunk_size = 1ULL << en->chunkbits; chunk_idx = loff >> en->chunkbits; chunk_off = loff & (chunk_size - 1); if ((en->chunkformat & EROFS_CHUNK_FORMAT_INDEXES) != 0) entry_size = sizeof(struct erofs_inode_chunk_index); else entry_size = EROFS_BLOCK_MAP_ENTRY_SIZE; if (en->inode_off > UINT64_MAX - en->inode_isize || en->inode_off + en->inode_isize > UINT64_MAX - en->xattr_isize) return (EOVERFLOW); idx_base = en->inode_off + en->inode_isize + en->xattr_isize; if (idx_base > UINT64_MAX - (entry_size - 1)) return (EOVERFLOW); idx_base = roundup2(idx_base, entry_size); if (chunk_idx > (UINT64_MAX - idx_base) / entry_size) return (EOVERFLOW); idx_off = idx_base + chunk_idx * entry_size; if (erofs_nid_in_metabox(en->nid)) { if (em->metabox_en == NULL || idx_off > em->metabox_en->size || entry_size > em->metabox_en->size - idx_off) return (EINTEGRITY); } else { if (em->blocks > (UINT64_MAX >> em->block_bits)) return (EOVERFLOW); image_size = em->blocks << em->block_bits; if (idx_off > image_size || entry_size > image_size - idx_off) return (EINTEGRITY); } error = erofs_read_metadata(em, en->nid, idx_off, entry_size, &buf); if (error != 0) return (error); idx = buf; if ((en->chunkformat & EROFS_CHUNK_FORMAT_INDEXES) != 0) { blkaddr = le32toh(idx->startblk_lo); if ((en->chunkformat & EROFS_CHUNK_FORMAT_48BIT) != 0) blkaddr |= (uint64_t)le16toh(idx->startblk_hi) << 32; raw_device_id = le16toh(idx->device_id); addrmask = (en->chunkformat & EROFS_CHUNK_FORMAT_48BIT) != 0 ? ((1ULL << 48) - 1) : UINT32_MAX; } else { blkaddr = le32toh(*(__le32 *)idx); raw_device_id = 0; addrmask = UINT32_MAX; } erofs_brelse(buf); if (!((blkaddr ^ EROFS_NULL_ADDR) & addrmask)) { *hole = true; *phys_off = 0; *run_len = MIN(chunk_size - chunk_off, en->size - loff); return (0); } *run_len = MIN(chunk_size - chunk_off, en->size - loff); *device_id = raw_device_id & em->device_id_mask; if (blkaddr > (UINT64_MAX >> em->block_bits)) return (EOVERFLOW); *phys_off = blkaddr << em->block_bits; if (chunk_off > UINT64_MAX - *phys_off) return (EOVERFLOW); *phys_off += chunk_off; *hole = false; return (0); } static int erofs_bread_device(struct erofs_mount *em, struct erofs_device_info *dif, erofs_blk_t blocks, uint64_t off, size_t len, void **bufp) { struct buf *bp; uint64_t end, limit; off_t blkoff, current; size_t blklen, done, iosize; char *out; int error; if (bufp == NULL) return (EINVAL); *bufp = NULL; if (len == 0) { return (0); } if (dif == NULL || dif->devvp == NULL || dif->cp == NULL) return (ENODEV); if (__builtin_add_overflow(off, (uint64_t)len, &end)) return (EINTEGRITY); if (blocks != 0) { if (blocks > (UINT64_MAX >> em->block_bits)) return (EINTEGRITY); limit = blocks << em->block_bits; if (end > limit) return (EINTEGRITY); } if (end > dif->mediasize) return (ENXIO); if (off > INT64_MAX || end > (uint64_t)INT64_MAX + 1) return (EOVERFLOW); iosize = em->block_size != 0 ? em->block_size : dif->sectorsize; if (iosize == 0 || (iosize & (iosize - 1)) != 0) return (EINVAL); out = malloc(len, M_EROFS, M_WAITOK); done = 0; while (done < len) { current = (off_t)(off + done); blkoff = rounddown2(current, (off_t)iosize); blklen = MIN(iosize - (size_t)(current - blkoff), len - done); error = bread(dif->devvp, btodb(blkoff), iosize, NOCRED, &bp); if (error != 0) { free(out, M_EROFS); return (error); } if (bp->b_data == NULL) { brelse(bp); free(out, M_EROFS); return (EIO); } memcpy(out + done, (char *)bp->b_data + (current - blkoff), blklen); brelse(bp); done += blklen; } *bufp = out; return (0); } int erofs_bread(struct erofs_mount *em, uint64_t off, size_t len, void **bufp) { return (erofs_bread_device(em, &em->dif0, em->dif0.blocks, off, len, bufp)); } int erofs_read_physical(struct erofs_mount *em, unsigned int device_id, uint64_t off, size_t len, void **bufp) { struct erofs_map_dev map; erofs_blk_t blocks; int error; map = (struct erofs_map_dev) { .m_pa = off, .m_plen = len, .m_deviceid = device_id, }; error = erofs_map_dev(em, &map); if (error != 0) return (error); blocks = map.m_dif->blocks; if (map.m_dif == &em->dif0 && em->flatdev) blocks = em->flatdev_blocks; return (erofs_bread_device(em, map.m_dif, blocks, map.m_pa, len, bufp)); } /* Release a contiguous buffer returned by erofs_bread(). */ void erofs_brelse(void *buf) { free(buf, M_EROFS); } /* Read inode metadata from either the primary image or the metabox file. */ int erofs_read_metadata(struct erofs_mount *em, erofs_nid_t nid, uint64_t off, size_t len, void **bufp) { if (!erofs_nid_in_metabox(nid)) { if (off > INT64_MAX) return (EOVERFLOW); return (erofs_bread(em, (off_t)off, len, bufp)); } if (!erofs_sb_has_metabox(em) || em->metabox_en == NULL) return (EINTEGRITY); return (erofs_read_data(em, em->metabox_en, off, len, bufp)); } /* * Map a logical file offset to a physical position for an uncompressed * plain/inline inode. * * Output: * - phys_off: physical byte offset; * - run_len: contiguous length readable from the current position; * - hole: whether the current range maps to a zero-filled hole (NULL_ADDR). */ int erofs_map_blocks(struct erofs_mount *em, struct erofs_node *en, uint64_t loff, uint64_t *phys_off, unsigned int *device_id, size_t *run_len, bool *hole, bool *metadata) { uint64_t tail_start, remain, block_rem; *phys_off = 0; *device_id = 0; *run_len = 0; *hole = false; *metadata = false; if (loff >= en->size) return (0); remain = en->size - loff; switch (en->datalayout) { case EROFS_INODE_CHUNK_BASED: return (erofs_map_blocks_chunk(em, en, loff, phys_off, device_id, run_len, hole)); case EROFS_INODE_FLAT_PLAIN: block_rem = em->block_size - (loff & (em->block_size - 1)); *run_len = MIN(remain, block_rem); if (en->startblk == EROFS_NULL_ADDR) { *hole = true; return (0); } if (en->startblk > (UINT64_MAX >> em->block_bits) || __builtin_add_overflow(en->startblk << em->block_bits, loff, phys_off)) return (EINTEGRITY); return (0); case EROFS_INODE_FLAT_INLINE: tail_start = erofs_inline_tail_start(em, en); if (loff < tail_start) { block_rem = em->block_size - (loff & (em->block_size - 1)); *run_len = MIN(MIN(remain, tail_start - loff), block_rem); if (en->startblk == EROFS_NULL_ADDR) { *hole = true; return (0); } if (en->startblk > (UINT64_MAX >> em->block_bits) || __builtin_add_overflow(en->startblk << em->block_bits, loff, phys_off)) return (EINTEGRITY); return (0); } block_rem = em->block_size - ((loff - tail_start) & (em->block_size - 1)); *run_len = MIN(remain, block_rem); if (__builtin_add_overflow(en->inode_off, en->inode_isize, phys_off) || __builtin_add_overflow(*phys_off, en->xattr_isize, phys_off) || __builtin_add_overflow(*phys_off, loff - tail_start, phys_off)) return (EINTEGRITY); *metadata = true; return (0); case EROFS_INODE_COMPRESSED_FULL: case EROFS_INODE_COMPRESSED_COMPACT: return (EOPNOTSUPP); default: return (EOPNOTSUPP); } } /* * Read a small range at a logical file offset into a contiguous buffer. * Primarily used for directory block reads, lookup, and symlink fragment * parsing. */ int erofs_read_data(struct erofs_mount *em, struct erofs_node *en, uint64_t loff, size_t len, void **bufp) { char *out; void *blk; uint64_t phys_off; unsigned int device_id; size_t run_len, done, want; bool hole, metadata; int error; if (bufp == NULL) return (EINVAL); *bufp = NULL; if (len == 0) { return (0); } if (loff > UINT64_MAX - (uint64_t)len) return (EOVERFLOW); if (loff > en->size || (uint64_t)len > en->size - loff) return (EINTEGRITY); /* Compressed file path */ if (en->datalayout == EROFS_INODE_COMPRESSED_FULL || en->datalayout == EROFS_INODE_COMPRESSED_COMPACT) return (z_erofs_read_data(em, en, loff, len, bufp)); /* Uncompressed file path */ out = malloc(len, M_EROFS, M_WAITOK); done = 0; while (done < len) { error = erofs_map_blocks(em, en, loff + done, &phys_off, &device_id, &run_len, &hole, &metadata); if (error != 0) { free(out, M_EROFS); return (error); } if (run_len == 0) { free(out, M_EROFS); return (EINTEGRITY); } want = MIN(run_len, len - done); if (hole) { bzero(out + done, want); } else { if (metadata) error = erofs_read_metadata(em, en->nid, phys_off, want, &blk); else error = erofs_read_physical(em, device_id, phys_off, want, &blk); if (error != 0) { free(out, M_EROFS); return (error); } memcpy(out + done, blk, want); erofs_brelse(blk); } done += want; } *bufp = out; return (0); } /* * Transfer the logical content of an inode directly into a uio. * Regular files and symlinks both use this read path. */ static int erofs_read_uio(struct erofs_mount *em, struct erofs_node *en, struct uio *uio) { char zerobuf[PAGE_SIZE]; void *blk; uint64_t phys_off; unsigned int device_id; size_t run_len, want, chunk; bool hole, metadata; int error; if (uio->uio_offset < 0) return (EINVAL); if ((uint64_t)uio->uio_offset >= en->size) return (0); /* Compressed file path */ if (en->datalayout == EROFS_INODE_COMPRESSED_FULL || en->datalayout == EROFS_INODE_COMPRESSED_COMPACT) return (z_erofs_read_uio(em, en, uio)); /* Uncompressed file path */ bzero(zerobuf, sizeof(zerobuf)); while (uio->uio_resid > 0 && (uint64_t)uio->uio_offset < en->size) { error = erofs_map_blocks(em, en, uio->uio_offset, &phys_off, &device_id, &run_len, &hole, &metadata); if (error != 0) return (error); if (run_len == 0) break; want = MIN(run_len, (size_t)uio->uio_resid); if (hole) { chunk = want; while (chunk > 0) { size_t zlen = MIN(chunk, sizeof(zerobuf)); error = uiomove(zerobuf, zlen, uio); if (error != 0) return (error); chunk -= zlen; } continue; } if (metadata) error = erofs_read_metadata(em, en->nid, phys_off, want, &blk); else error = erofs_read_physical(em, device_id, phys_off, want, &blk); if (error != 0) return (error); error = uiomove(blk, want, uio); erofs_brelse(blk); if (error != 0) return (error); } return (0); } /* Read symlink target string. */ int erofs_readlink_target(struct vnode *vp, struct uio *uio) { return (erofs_read_uio(MTOE(vp->v_mount), VTOE(vp), uio)); } /* Read regular file data. */ int erofs_read_file(struct vnode *vp, struct uio *uio, int ioflag) { (void)ioflag; return (erofs_read_uio(MTOE(vp->v_mount), VTOE(vp), uio)); }