442 lines
12 KiB
C
442 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
|
* https://www.huawei.com/
|
|
* Copyright (C) 2022, Alibaba Cloud
|
|
*/
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/systm.h>
|
|
#include <sys/dirent.h>
|
|
#include <sys/kernel.h>
|
|
#include <sys/libkern.h>
|
|
#include <sys/limits.h>
|
|
#include <sys/malloc.h>
|
|
#include <sys/mount.h>
|
|
#include <sys/vnode.h>
|
|
|
|
#include "internal.h"
|
|
|
|
/* Map EROFS directory entry file type to FreeBSD dirent.d_type. */
|
|
static unsigned char
|
|
erofs_ftype_to_dtype(uint8_t ftype)
|
|
{
|
|
switch (ftype) {
|
|
case EROFS_FT_REG_FILE:
|
|
return (DT_REG);
|
|
case EROFS_FT_DIR:
|
|
return (DT_DIR);
|
|
case EROFS_FT_CHRDEV:
|
|
return (DT_CHR);
|
|
case EROFS_FT_BLKDEV:
|
|
return (DT_BLK);
|
|
case EROFS_FT_FIFO:
|
|
return (DT_FIFO);
|
|
case EROFS_FT_SOCK:
|
|
return (DT_SOCK);
|
|
case EROFS_FT_SYMLINK:
|
|
return (DT_LNK);
|
|
default:
|
|
return (DT_UNKNOWN);
|
|
}
|
|
}
|
|
|
|
static int
|
|
erofs_dirname_order(const char *left, size_t leftlen, const char *right,
|
|
size_t rightlen)
|
|
{
|
|
size_t common;
|
|
int order;
|
|
|
|
common = MIN(leftlen, rightlen);
|
|
order = memcmp(left, right, common);
|
|
if (order != 0)
|
|
return (order);
|
|
if (leftlen == rightlen)
|
|
return (0);
|
|
return (leftlen < rightlen ? -1 : 1);
|
|
}
|
|
|
|
/* Validate one name slot and return its Linux-visible length. */
|
|
int
|
|
erofs_dirent_namelen(const char *blk, uint32_t nameoff, uint32_t endoff,
|
|
bool trailing, size_t *namelenp)
|
|
{
|
|
size_t namelen, span;
|
|
|
|
if (endoff <= nameoff)
|
|
return (EINTEGRITY);
|
|
span = endoff - nameoff;
|
|
if (trailing) {
|
|
namelen = strnlen(blk + nameoff, span);
|
|
} else {
|
|
namelen = span;
|
|
if (memchr(blk + nameoff, '\0', span) != NULL)
|
|
return (EINTEGRITY);
|
|
}
|
|
if (namelen == 0 || namelen > EROFS_NAME_LEN)
|
|
return (EINTEGRITY);
|
|
for (size_t i = 0; i < namelen; i++) {
|
|
if (blk[nameoff + i] == '/')
|
|
return (EINTEGRITY);
|
|
}
|
|
*namelenp = namelen;
|
|
return (0);
|
|
}
|
|
|
|
/* Validate the dirent array, name offsets, and names in a block. */
|
|
int
|
|
erofs_validate_dirblock(const char *blk, uint32_t blksz, uint32_t maxsize,
|
|
uint32_t *ndirentsp)
|
|
{
|
|
const struct erofs_dirent *de;
|
|
uint32_t endoff, first_nameoff, idx, nameoff, ndirents, prev_nameoff;
|
|
size_t namelen, prev_namelen;
|
|
int error;
|
|
|
|
if (blksz < sizeof(struct erofs_dirent) ||
|
|
maxsize < sizeof(struct erofs_dirent) ||
|
|
maxsize > blksz)
|
|
return (EINTEGRITY);
|
|
de = (const struct erofs_dirent *)blk;
|
|
first_nameoff = le16toh(de[0].nameoff);
|
|
if (first_nameoff < sizeof(struct erofs_dirent) ||
|
|
first_nameoff >= maxsize ||
|
|
(first_nameoff % sizeof(struct erofs_dirent)) != 0)
|
|
return (EINTEGRITY);
|
|
ndirents = first_nameoff / sizeof(struct erofs_dirent);
|
|
prev_nameoff = 0;
|
|
prev_namelen = 0;
|
|
for (idx = 0; idx < ndirents; idx++) {
|
|
nameoff = le16toh(de[idx].nameoff);
|
|
if ((idx == 0 && nameoff != first_nameoff) ||
|
|
(idx != 0 && nameoff <= prev_nameoff) ||
|
|
nameoff < first_nameoff || nameoff >= maxsize)
|
|
return (EINTEGRITY);
|
|
endoff = idx + 1 < ndirents ?
|
|
le16toh(de[idx + 1].nameoff) : maxsize;
|
|
if (endoff <= nameoff || endoff > maxsize)
|
|
return (EINTEGRITY);
|
|
error = erofs_dirent_namelen(blk, nameoff, endoff,
|
|
idx + 1 == ndirents, &namelen);
|
|
if (error != 0)
|
|
return (error);
|
|
if (idx != 0 && erofs_dirname_order(blk + prev_nameoff,
|
|
prev_namelen, blk + nameoff, namelen) >= 0)
|
|
return (EINTEGRITY);
|
|
prev_nameoff = nameoff;
|
|
prev_namelen = namelen;
|
|
}
|
|
*ndirentsp = ndirents;
|
|
return (0);
|
|
}
|
|
|
|
/* Extract name, nid, and type of the idx'th directory entry from a block. */
|
|
static int
|
|
erofs_dirent_name(const char *blk, uint32_t maxsize,
|
|
uint32_t idx, uint32_t ndirents, char *name, size_t namesz, erofs_nid_t *nid,
|
|
uint8_t *ftype, size_t *namelenp)
|
|
{
|
|
const struct erofs_dirent *de;
|
|
uint32_t nameoff, endoff;
|
|
size_t namelen;
|
|
int error;
|
|
|
|
de = (const struct erofs_dirent *)blk;
|
|
nameoff = le16toh(de[idx].nameoff);
|
|
if (idx + 1 < ndirents)
|
|
endoff = le16toh(de[idx + 1].nameoff);
|
|
else
|
|
endoff = maxsize;
|
|
error = erofs_dirent_namelen(blk, nameoff, endoff,
|
|
idx + 1 == ndirents, &namelen);
|
|
if (error != 0)
|
|
return (error);
|
|
if (namelen >= namesz)
|
|
return (EINTEGRITY);
|
|
memcpy(name, blk + nameoff, namelen);
|
|
name[namelen] = '\0';
|
|
*nid = le64toh(de[idx].nid);
|
|
*ftype = de[idx].file_type;
|
|
*namelenp = namelen;
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
erofs_previous_dirname(struct erofs_sb_info *sbi, struct erofs_inode *dir,
|
|
erofs_off_t block_off, char *name, size_t namesz, size_t *namelenp)
|
|
{
|
|
erofs_nid_t nid;
|
|
uint32_t maxsize, ndirents;
|
|
uint8_t ftype;
|
|
char *blk;
|
|
int error;
|
|
|
|
block_off -= sbi->block_size;
|
|
maxsize = MIN((uint64_t)sbi->block_size, dir->size - block_off);
|
|
error = erofs_read_data(sbi, dir, block_off, maxsize, (void **)&blk);
|
|
if (error != 0)
|
|
return (error);
|
|
error = erofs_validate_dirblock(blk, sbi->block_size, maxsize,
|
|
&ndirents);
|
|
if (error == 0)
|
|
error = erofs_dirent_name(blk, maxsize, ndirents - 1,
|
|
ndirents, name, namesz, &nid, &ftype, namelenp);
|
|
erofs_brelse(blk);
|
|
return (error);
|
|
}
|
|
|
|
/* Per-call state for readdir dirent/cookie output. */
|
|
struct erofs_uiodir {
|
|
struct dirent *dirent;
|
|
uint64_t *cookies;
|
|
uint64_t last_cookie;
|
|
int ncookies;
|
|
int acookies;
|
|
int eofflag;
|
|
};
|
|
|
|
enum erofs_uiodir_result {
|
|
EROFS_UIODIR_BUFFER_FULL = -1,
|
|
EROFS_UIODIR_OK = 0,
|
|
};
|
|
|
|
/* Push a dirent and its cookie to the caller, modelled after UDF. */
|
|
static int
|
|
erofs_uiodir(struct erofs_uiodir *uiodir, int de_size, struct uio *uio,
|
|
uint64_t cookie)
|
|
{
|
|
int error;
|
|
|
|
if (cookie <= uiodir->last_cookie)
|
|
return (EINTEGRITY);
|
|
if (uio->uio_resid < de_size ||
|
|
(uiodir->cookies != NULL &&
|
|
uiodir->acookies >= uiodir->ncookies)) {
|
|
return (EROFS_UIODIR_BUFFER_FULL);
|
|
}
|
|
error = uiomove(uiodir->dirent, de_size, uio);
|
|
if (error != 0)
|
|
return (error);
|
|
uiodir->last_cookie = cookie;
|
|
if (uiodir->cookies != NULL)
|
|
uiodir->cookies[uiodir->acookies++] = cookie;
|
|
return (EROFS_UIODIR_OK);
|
|
}
|
|
|
|
/*
|
|
* Process directory entries within a single block and output them to uio.
|
|
* (Linux equivalent: erofs_fill_dentries in Linux's dir.c)
|
|
*
|
|
* Returns 0 on success (all entries consumed), -1 if uio is full, or a
|
|
* positive error code on corruption.
|
|
*/
|
|
static int
|
|
erofs_fill_dentries(struct erofs_sb_info *sbi, struct erofs_uiodir *uiodir,
|
|
struct uio *uio, struct dirent *d, const char *blk, uint32_t maxsize,
|
|
uint32_t start_idx, uint32_t ndirents, erofs_off_t block_off,
|
|
uint64_t *logical_offp)
|
|
{
|
|
char name[EROFS_NAME_LEN + 1];
|
|
uint32_t idx;
|
|
uint64_t curpos, nextoff;
|
|
erofs_nid_t nid;
|
|
size_t namelen;
|
|
uint8_t ftype;
|
|
int error;
|
|
|
|
for (idx = start_idx; idx < ndirents; idx++) {
|
|
curpos = block_off + idx * sizeof(struct erofs_dirent);
|
|
nextoff = (idx + 1 < ndirents) ?
|
|
(curpos + sizeof(struct erofs_dirent)) :
|
|
(block_off + maxsize);
|
|
error = erofs_dirent_name(blk, maxsize, idx, ndirents, name,
|
|
sizeof(name), &nid, &ftype, &namelen);
|
|
if (error != 0)
|
|
return (error);
|
|
if (!erofs_nid_is_valid(sbi, nid))
|
|
return (EINTEGRITY);
|
|
bzero(d, sizeof(*d));
|
|
d->d_fileno = nid;
|
|
d->d_type = erofs_ftype_to_dtype(ftype);
|
|
d->d_namlen = namelen;
|
|
d->d_reclen = GENERIC_DIRSIZ(d);
|
|
d->d_off = nextoff;
|
|
strlcpy(d->d_name, name, sizeof(d->d_name));
|
|
error = erofs_uiodir(uiodir, d->d_reclen, uio, d->d_off);
|
|
if (error != 0)
|
|
return (error);
|
|
*logical_offp = nextoff;
|
|
uio->uio_offset = *logical_offp;
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Read directory contents and output a FreeBSD dirent stream to uio.
|
|
*
|
|
* Key points:
|
|
* - On-disk entries use their logical file offsets as cookies;
|
|
* - A dot_omitted directory appends a synthetic "." at i_size, matching
|
|
* Linux, so existing on-disk cookies are not shifted;
|
|
* - The dirent array occupies only the front portion of a block, so after
|
|
* scanning all entries offset must jump to maxsize (the block end),
|
|
* otherwise the loop would get stuck on the same block;
|
|
* - Supports a_ncookies / a_cookies for NFS and other callers that need
|
|
* resumable iteration.
|
|
*/
|
|
int
|
|
erofs_readdir_block(struct vnode *vp, struct uio *uio, int *eofflag,
|
|
int *ncookies, uint64_t **cookies)
|
|
{
|
|
struct erofs_inode *dir;
|
|
struct erofs_sb_info *sbi;
|
|
struct erofs_uiodir uiodir;
|
|
struct dirent d;
|
|
uint64_t *cookiebuf;
|
|
size_t cookie_count;
|
|
char first_name[EROFS_NAME_LEN + 1];
|
|
char previous_name[EROFS_NAME_LEN + 1];
|
|
char *blk;
|
|
erofs_off_t block_off;
|
|
uint64_t logical_off;
|
|
uint32_t block_pos, blksz, ndirents, start_idx, maxsize;
|
|
erofs_nid_t edge_nid;
|
|
size_t first_namelen, previous_namelen;
|
|
uint8_t edge_ftype;
|
|
bool have_previous, sequential;
|
|
int error;
|
|
|
|
dir = VTOE(vp);
|
|
sbi = MTOE(vp->v_mount);
|
|
blksz = sbi->block_size;
|
|
error = 0;
|
|
cookiebuf = NULL;
|
|
cookie_count = 0;
|
|
have_previous = false;
|
|
uiodir.eofflag = 0;
|
|
uiodir.acookies = 0;
|
|
uiodir.dirent = &d;
|
|
uiodir.cookies = NULL;
|
|
uiodir.ncookies = 0;
|
|
if (cookies != NULL && ncookies != NULL) {
|
|
*cookies = NULL;
|
|
*ncookies = 0;
|
|
if (uio->uio_resid > 0) {
|
|
cookie_count = (size_t)uio->uio_resid /
|
|
GENERIC_MINDIRSIZ;
|
|
cookie_count = MIN(cookie_count, (size_t)INT_MAX);
|
|
cookie_count = MIN(cookie_count,
|
|
SIZE_MAX / sizeof(*cookiebuf));
|
|
}
|
|
uiodir.ncookies = (int)cookie_count;
|
|
if (cookie_count != 0)
|
|
cookiebuf = malloc(sizeof(*cookiebuf) * cookie_count,
|
|
M_TEMP, M_WAITOK);
|
|
uiodir.cookies = cookiebuf;
|
|
}
|
|
|
|
if (uio->uio_offset < 0) {
|
|
error = EINVAL;
|
|
goto out;
|
|
}
|
|
if (dir->dot_omitted && dir->size == (uint64_t)OFF_MAX) {
|
|
error = EINTEGRITY;
|
|
goto out;
|
|
}
|
|
|
|
logical_off = uio->uio_offset;
|
|
sequential = logical_off == 0;
|
|
uiodir.last_cookie = logical_off;
|
|
uio->uio_offset = logical_off;
|
|
|
|
while (logical_off < dir->size) {
|
|
block_off = rounddown2(logical_off, (uint64_t)blksz);
|
|
maxsize = MIN((uint64_t)blksz, dir->size - block_off);
|
|
block_pos = logical_off - block_off;
|
|
if ((block_pos % sizeof(struct erofs_dirent)) != 0) {
|
|
block_pos = roundup(block_pos, sizeof(struct erofs_dirent));
|
|
logical_off = block_off + block_pos;
|
|
uio->uio_offset = logical_off;
|
|
}
|
|
error = erofs_read_data_readahead(sbi, dir, block_off,
|
|
maxsize, sequential, (void **)&blk);
|
|
if (error != 0)
|
|
goto out;
|
|
error = erofs_validate_dirblock(blk, blksz, maxsize, &ndirents);
|
|
if (error != 0) {
|
|
erofs_brelse(blk);
|
|
goto out;
|
|
}
|
|
if (!have_previous && block_off != 0) {
|
|
error = erofs_previous_dirname(sbi, dir, block_off,
|
|
previous_name, sizeof(previous_name),
|
|
&previous_namelen);
|
|
if (error != 0) {
|
|
erofs_brelse(blk);
|
|
goto out;
|
|
}
|
|
have_previous = true;
|
|
}
|
|
error = erofs_dirent_name(blk, maxsize, 0, ndirents,
|
|
first_name, sizeof(first_name), &edge_nid, &edge_ftype,
|
|
&first_namelen);
|
|
if (error == 0 && have_previous &&
|
|
erofs_dirname_order(previous_name, previous_namelen,
|
|
first_name, first_namelen) >= 0)
|
|
error = EINTEGRITY;
|
|
if (error == 0)
|
|
error = erofs_dirent_name(blk, maxsize, ndirents - 1,
|
|
ndirents, previous_name, sizeof(previous_name),
|
|
&edge_nid, &edge_ftype, &previous_namelen);
|
|
if (error != 0) {
|
|
erofs_brelse(blk);
|
|
goto out;
|
|
}
|
|
have_previous = true;
|
|
start_idx = block_pos / sizeof(struct erofs_dirent);
|
|
if (start_idx >= ndirents) {
|
|
logical_off = block_off + maxsize;
|
|
uio->uio_offset = logical_off;
|
|
erofs_brelse(blk);
|
|
continue;
|
|
}
|
|
error = erofs_fill_dentries(sbi, &uiodir, uio, &d, blk, maxsize,
|
|
start_idx, ndirents, block_off, &logical_off);
|
|
erofs_brelse(blk);
|
|
if (error != 0)
|
|
goto out;
|
|
}
|
|
if (dir->dot_omitted && logical_off == dir->size) {
|
|
bzero(&d, sizeof(d));
|
|
d.d_fileno = dir->nid;
|
|
d.d_type = DT_DIR;
|
|
d.d_namlen = 1;
|
|
d.d_reclen = GENERIC_DIRSIZ(&d);
|
|
d.d_off = dir->size + 1;
|
|
d.d_name[0] = '.';
|
|
d.d_name[1] = '\0';
|
|
error = erofs_uiodir(&uiodir, d.d_reclen, uio, d.d_off);
|
|
if (error != 0)
|
|
goto out;
|
|
logical_off++;
|
|
uio->uio_offset = logical_off;
|
|
}
|
|
uiodir.eofflag = 1;
|
|
out:
|
|
if (error == EROFS_UIODIR_BUFFER_FULL)
|
|
error = 0;
|
|
if (eofflag != NULL && error == 0)
|
|
*eofflag = uiodir.eofflag;
|
|
if (cookies != NULL && ncookies != NULL) {
|
|
if (error != 0) {
|
|
if (cookiebuf != NULL)
|
|
free(cookiebuf, M_TEMP);
|
|
} else {
|
|
*ncookies = uiodir.acookies;
|
|
*cookies = cookiebuf;
|
|
}
|
|
}
|
|
return (error);
|
|
}
|