// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2017-2018 HUAWEI, Inc. * https://www.huawei.com/ * Copyright (C) 2022, Alibaba Cloud */ #include #include #include #include #include #include #include #include #include #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); }