This commit is contained in:
2026-08-14 12:01:38 +02:00
commit 0bbc249155
21 changed files with 6786 additions and 0 deletions
+542
View File
@@ -0,0 +1,542 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2017-2018 HUAWEI, Inc.
* https://www.huawei.com/
* Copyright (C) 2021, Alibaba Cloud
*/
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bio.h>
#include <sys/buf.h>
#include <sys/kernel.h>
#include <sys/libkern.h>
#include <sys/malloc.h>
#include <sys/mount.h>
#include <sys/vnode.h>
#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));
}