// SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0 #include #include #include "erofs/print.h" #include "erofs/dir.h" /* filename should not have a '/' in the name string */ static bool erofs_validate_filename(const char *dname, int size) { char *name = (char *)dname; while (name - dname < size && *name != '\0') { if (*name == '/') return false; ++name; } return true; } static int traverse_dirents(struct erofs_dir_context *ctx, void *dentry_blk, unsigned int lblk, unsigned int next_nameoff, unsigned int maxsize, bool fsck) { struct erofs_sb_info *sbi = ctx->dir->sbi; struct erofs_dirent *de = dentry_blk; const struct erofs_dirent *end = dentry_blk + next_nameoff; const char *prev_name = NULL; const char *errmsg; unsigned int prev_namelen = 0; int ret = 0; bool silent = false; while (de < end) { const char *de_name; unsigned int de_namelen; unsigned int nameoff; nameoff = le16_to_cpu(de->nameoff); de_name = (char *)dentry_blk + nameoff; /* the last dirent check */ if (de + 1 >= end) de_namelen = strnlen(de_name, maxsize - nameoff); else de_namelen = le16_to_cpu(de[1].nameoff) - nameoff; ctx->de_nid = le64_to_cpu(de->nid); erofs_dbg("traversed nid (%llu)", ctx->de_nid | 0ULL); ret = -EFSCORRUPTED; /* corrupted entry check */ if (nameoff != next_nameoff) { errmsg = "bogus dirent nameoff"; break; } if (nameoff + de_namelen > maxsize || !de_namelen || de_namelen > EROFS_NAME_LEN) { errmsg = "bogus dirent namelen"; break; } if (fsck && prev_name) { int cmp = strncmp(prev_name, de_name, min(prev_namelen, de_namelen)); if (cmp > 0 || (cmp == 0 && prev_namelen >= de_namelen)) { errmsg = "wrong dirent name order"; break; } } if (fsck && de->file_type >= EROFS_FT_MAX) { errmsg = "invalid file type %u"; break; } ctx->dname = de_name; ctx->de_namelen = de_namelen; ctx->de_ftype = de->file_type; ctx->dot_dotdot = is_dot_dotdot_len(de_name, de_namelen); if (ctx->dot_dotdot) { switch (de_namelen) { case 2: if (fsck && (ctx->flags & EROFS_READDIR_DOTDOT_FOUND)) { errmsg = "duplicated `..' dirent"; goto out; } ctx->flags |= EROFS_READDIR_DOTDOT_FOUND; if (sbi->root_nid == ctx->dir->nid) { ctx->pnid = sbi->root_nid; ctx->flags |= EROFS_READDIR_VALID_PNID; } if (fsck && (ctx->flags & EROFS_READDIR_VALID_PNID) && ctx->de_nid != ctx->pnid) { errmsg = "corrupted `..' dirent"; goto out; } break; case 1: if (fsck && (ctx->flags & EROFS_READDIR_DOT_FOUND)) { errmsg = "duplicated `.' dirent"; goto out; } ctx->flags |= EROFS_READDIR_DOT_FOUND; if (fsck && ctx->de_nid != ctx->dir->nid) { errmsg = "corrupted `.' dirent"; goto out; } break; } } else if (fsck && !erofs_validate_filename(de_name, de_namelen)) { errmsg = "corrupted dirent with illegal filename"; goto out; } ret = ctx->cb(ctx); if (ret) { silent = true; break; } prev_name = de_name; prev_namelen = de_namelen; next_nameoff += de_namelen; ++de; } out: if (ret && !silent) erofs_err("%s @ nid %llu, lblk %u, index %lu", errmsg, ctx->dir->nid | 0ULL, lblk, (de - (struct erofs_dirent *)dentry_blk) | 0UL); return ret; } int erofs_iterate_dir(struct erofs_dir_context *ctx, bool fsck) { struct erofs_inode *dir = ctx->dir; struct erofs_sb_info *sbi = dir->sbi; int err = 0; erofs_off_t pos; char buf[EROFS_MAX_BLOCK_SIZE]; if (!S_ISDIR(dir->i_mode)) return -ENOTDIR; ctx->flags &= ~EROFS_READDIR_ALL_SPECIAL_FOUND; pos = 0; while (pos < dir->i_size) { erofs_blk_t lblk = erofs_blknr(sbi, pos); erofs_off_t maxsize = min_t(erofs_off_t, dir->i_size - pos, erofs_blksiz(sbi)); const struct erofs_dirent *de = (const void *)buf; unsigned int nameoff; err = erofs_pread(dir, buf, maxsize, pos); if (err) { erofs_err("I/O error occurred when reading dirents @ nid %llu, lblk %u: %d", dir->nid | 0ULL, lblk, err); return err; } nameoff = le16_to_cpu(de->nameoff); if (nameoff < sizeof(struct erofs_dirent) || nameoff >= erofs_blksiz(sbi)) { erofs_err("invalid de[0].nameoff %u @ nid %llu, lblk %u", nameoff, dir->nid | 0ULL, lblk); return -EFSCORRUPTED; } err = traverse_dirents(ctx, buf, lblk, nameoff, maxsize, fsck); if (err) break; pos += maxsize; } if (fsck && (ctx->flags & EROFS_READDIR_ALL_SPECIAL_FOUND) != EROFS_READDIR_ALL_SPECIAL_FOUND) { erofs_err("`.' or `..' dirent is missing @ nid %llu", dir->nid | 0ULL); return -EFSCORRUPTED; } return err; } #define EROFS_PATHNAME_FOUND 1 struct erofs_get_pathname_context { struct erofs_dir_context ctx; erofs_nid_t target_nid; char *buf; size_t size; size_t pos; }; static int erofs_get_pathname_iter(struct erofs_dir_context *ctx) { int ret; struct erofs_get_pathname_context *pathctx = (void *)ctx; const char *dname = ctx->dname; size_t len = ctx->de_namelen; size_t pos = pathctx->pos; if (ctx->dot_dotdot) return 0; if (ctx->de_nid == pathctx->target_nid) { if (pos + len + 2 > pathctx->size) { erofs_err("get_pathname buffer not large enough: len %zd, size %zd", pos + len + 2, pathctx->size); return -ERANGE; } pathctx->buf[pos++] = '/'; strncpy(pathctx->buf + pos, dname, len); pathctx->buf[pos + len] = '\0'; return EROFS_PATHNAME_FOUND; } if (ctx->de_ftype == EROFS_FT_DIR || ctx->de_ftype == EROFS_FT_UNKNOWN) { struct erofs_inode dir = { .sbi = ctx->dir->sbi, .nid = ctx->de_nid }; ret = erofs_read_inode_from_disk(&dir); if (ret) { erofs_err("read inode failed @ nid %llu", dir.nid | 0ULL); return ret; } if (S_ISDIR(dir.i_mode)) { struct erofs_get_pathname_context nctx = { .ctx.flags = 0, .ctx.dir = &dir, .ctx.cb = erofs_get_pathname_iter, .target_nid = pathctx->target_nid, .buf = pathctx->buf, .size = pathctx->size, .pos = pos + len + 1, }; ret = erofs_iterate_dir(&nctx.ctx, false); if (ret == EROFS_PATHNAME_FOUND) { pathctx->buf[pos++] = '/'; strncpy(pathctx->buf + pos, dname, len); } return ret; } else if (ctx->de_ftype == EROFS_FT_DIR) { erofs_err("i_mode and file_type are inconsistent @ nid %llu", dir.nid | 0ULL); } } return 0; } int erofs_get_pathname(struct erofs_sb_info *sbi, erofs_nid_t nid, char *buf, size_t size) { int ret; struct erofs_inode root = { .sbi = sbi, .nid = sbi->root_nid, }; struct erofs_get_pathname_context pathctx = { .ctx.flags = 0, .ctx.dir = &root, .ctx.cb = erofs_get_pathname_iter, .target_nid = nid, .buf = buf, .size = size, .pos = 0, }; if (nid == root.nid) { if (size < 2) { erofs_err("get_pathname buffer not large enough: len 2, size %zd", size); return -ERANGE; } buf[0] = '/'; buf[1] = '\0'; return 0; } ret = erofs_read_inode_from_disk(&root); if (ret) { erofs_err("read inode failed @ nid %llu", root.nid | 0ULL); return ret; } ret = erofs_iterate_dir(&pathctx.ctx, false); if (ret == EROFS_PATHNAME_FOUND) return 0; if (!ret) return -ENOENT; return ret; }