// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "internal.h" #include "xattr.h" MALLOC_DEFINE(M_EROFS, "erofs", "EROFS filesystem"); static const uint32_t erofs_crc32c_seed = 0x5045b54aU; #define EROFS_STREAM_MOUNT_HARD_BUDGET (32UL * 1024 * 1024) #define EROFS_STREAM_GLOBAL_HARD_BUDGET (128UL * 1024 * 1024) #define EROFS_STREAM_ALLOCATION_HARD_MAX (4UL * 1024 * 1024) #define EROFS_STREAM_MOUNT_HARD_CONTEXTS (16U) #define EROFS_STREAM_GLOBAL_HARD_CONTEXTS (64U) #define EROFS_STREAM_MOUNT_CACHED_PER_CODEC (2U) #define EROFS_STREAM_GLOBAL_CACHED_PER_CODEC (16U) static unsigned long erofs_stream_mount_budget = EROFS_STREAM_MOUNT_HARD_BUDGET; static unsigned long erofs_stream_global_budget = EROFS_STREAM_GLOBAL_HARD_BUDGET; static int erofs_stream_mount_contexts = EROFS_STREAM_MOUNT_HARD_CONTEXTS; static int erofs_stream_global_contexts_limit = EROFS_STREAM_GLOBAL_HARD_CONTEXTS; static int erofs_stream_mount_cached = EROFS_STREAM_MOUNT_CACHED_PER_CODEC; static int erofs_stream_global_cached = EROFS_STREAM_GLOBAL_CACHED_PER_CODEC; TUNABLE_ULONG("vfs.erofs.stream_pool.mount_budget", &erofs_stream_mount_budget); TUNABLE_ULONG("vfs.erofs.stream_pool.global_budget", &erofs_stream_global_budget); TUNABLE_INT("vfs.erofs.stream_pool.mount_contexts", &erofs_stream_mount_contexts); TUNABLE_INT("vfs.erofs.stream_pool.global_contexts", &erofs_stream_global_contexts_limit); TUNABLE_INT("vfs.erofs.stream_pool.mount_cached_per_codec", &erofs_stream_mount_cached); TUNABLE_INT("vfs.erofs.stream_pool.global_cached_per_codec", &erofs_stream_global_cached); static struct mtx erofs_stream_lock; static LIST_HEAD(, erofs_sb_info) erofs_stream_mounts = LIST_HEAD_INITIALIZER(erofs_stream_mounts); static uma_zone_t erofs_stream_zone; static eventhandler_tag erofs_stream_lowmem_tag; static size_t erofs_stream_global_resident; static unsigned int erofs_stream_global_contexts; static unsigned int erofs_stream_global_cached_by_codec[ Z_EROFS_COMPRESSION_MAX]; static unsigned long erofs_stream_cached_contexts; static unsigned long erofs_stream_borrowed_contexts; static unsigned long erofs_stream_idle_contexts; static unsigned long erofs_stream_creations; static unsigned long erofs_stream_reuses; static unsigned long erofs_stream_fallbacks; static unsigned long erofs_stream_exhaustions; static unsigned long erofs_stream_allocation_failures; static unsigned long erofs_stream_destroys; static unsigned long erofs_stream_reclaims; MTX_SYSINIT(erofs_stream_pool, &erofs_stream_lock, "erofs stream pool", MTX_DEF); SYSCTL_NODE(_vfs, OID_AUTO, erofs, CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "EROFS filesystem"); SYSCTL_NODE(_vfs_erofs, OID_AUTO, stream_pool, CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "EROFS stream context pool"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, mount_budget, CTLFLAG_RD, &erofs_stream_mount_budget, 0, "Effective per-mount byte budget"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, global_budget, CTLFLAG_RD, &erofs_stream_global_budget, 0, "Effective global byte budget"); SYSCTL_INT(_vfs_erofs_stream_pool, OID_AUTO, mount_contexts, CTLFLAG_RD, &erofs_stream_mount_contexts, 0, "Effective per-mount context limit"); SYSCTL_INT(_vfs_erofs_stream_pool, OID_AUTO, global_contexts_limit, CTLFLAG_RD, &erofs_stream_global_contexts_limit, 0, "Effective global context limit"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, resident_bytes, CTLFLAG_RD, &erofs_stream_global_resident, 0, "Currently charged global bytes"); SYSCTL_UINT(_vfs_erofs_stream_pool, OID_AUTO, contexts, CTLFLAG_RD, &erofs_stream_global_contexts, 0, "Currently allocated contexts"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, cached, CTLFLAG_RD, &erofs_stream_cached_contexts, 0, "Currently cacheable contexts"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, borrowed, CTLFLAG_RD, &erofs_stream_borrowed_contexts, 0, "Currently borrowed contexts"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, idle, CTLFLAG_RD, &erofs_stream_idle_contexts, 0, "Currently idle contexts"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, creations, CTLFLAG_RD, &erofs_stream_creations, 0, "Successful context creations"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, reuses, CTLFLAG_RD, &erofs_stream_reuses, 0, "Idle context acquisitions"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, fallbacks, CTLFLAG_RD, &erofs_stream_fallbacks, 0, "Bounded temporary context fallbacks"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, exhaustions, CTLFLAG_RD, &erofs_stream_exhaustions, 0, "Hard-limit allocation rejections"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, allocation_failures, CTLFLAG_RD, &erofs_stream_allocation_failures, 0, "UMA or malloc allocation failures"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, destroys, CTLFLAG_RD, &erofs_stream_destroys, 0, "Destroyed contexts"); SYSCTL_ULONG(_vfs_erofs_stream_pool, OID_AUTO, reclaims, CTLFLAG_RD, &erofs_stream_reclaims, 0, "Low-memory idle context reclaims"); struct erofs_stream_alloc_header { struct erofs_stream_ctx *ctx; size_t bytes; }; static bool erofs_stream_reserve_locked(struct erofs_sb_info *sbi, size_t bytes) { struct erofs_stream_pool *pool; mtx_assert(&erofs_stream_lock, MA_OWNED); pool = &sbi->stream_pool; if (bytes > erofs_stream_mount_budget || pool->resident_bytes > erofs_stream_mount_budget - bytes || bytes > erofs_stream_global_budget || erofs_stream_global_resident > erofs_stream_global_budget - bytes) return (false); pool->resident_bytes += bytes; erofs_stream_global_resident += bytes; return (true); } static void erofs_stream_release_locked(struct erofs_sb_info *sbi, size_t bytes) { mtx_assert(&erofs_stream_lock, MA_OWNED); KASSERT(bytes <= sbi->stream_pool.resident_bytes && bytes <= erofs_stream_global_resident, ("erofs stream pool byte accounting underflow")); sbi->stream_pool.resident_bytes -= bytes; erofs_stream_global_resident -= bytes; } int z_erofs_stream_ctx_charge(struct erofs_stream_ctx *ctx, size_t bytes) { bool reserved; if (bytes == 0 || bytes > EROFS_STREAM_ALLOCATION_HARD_MAX) { ctx->allocation_failed = true; mtx_lock(&erofs_stream_lock); ++erofs_stream_exhaustions; mtx_unlock(&erofs_stream_lock); return (ENOMEM); } mtx_lock(&erofs_stream_lock); reserved = ctx->charged_bytes <= SIZE_MAX - bytes && erofs_stream_reserve_locked(ctx->sbi, bytes); if (reserved) ctx->charged_bytes += bytes; else ++erofs_stream_exhaustions; mtx_unlock(&erofs_stream_lock); if (!reserved) { ctx->allocation_failed = true; return (ENOMEM); } return (0); } void z_erofs_stream_ctx_uncharge(struct erofs_stream_ctx *ctx, size_t bytes) { mtx_lock(&erofs_stream_lock); KASSERT(bytes <= ctx->charged_bytes, ("erofs stream context byte accounting underflow")); ctx->charged_bytes -= bytes; erofs_stream_release_locked(ctx->sbi, bytes); mtx_unlock(&erofs_stream_lock); } void * z_erofs_stream_ctx_alloc(struct erofs_stream_ctx *ctx, size_t bytes) { struct erofs_stream_alloc_header *header; size_t total; if (__builtin_add_overflow(bytes, sizeof(*header), &total) || z_erofs_stream_ctx_charge(ctx, total) != 0) return (NULL); header = malloc(total, M_EROFS, M_NOWAIT | M_ZERO); if (header == NULL) { z_erofs_stream_ctx_uncharge(ctx, total); ctx->allocation_failed = true; mtx_lock(&erofs_stream_lock); ++erofs_stream_allocation_failures; mtx_unlock(&erofs_stream_lock); return (NULL); } header->ctx = ctx; header->bytes = total; return (header + 1); } void z_erofs_stream_ctx_free(struct erofs_stream_ctx *ctx, void *address) { struct erofs_stream_alloc_header *header; size_t bytes; if (address == NULL) return; header = (struct erofs_stream_alloc_header *)address - 1; KASSERT(header->ctx == ctx, ("erofs stream allocation owner mismatch")); bytes = header->bytes; free(header, M_EROFS); z_erofs_stream_ctx_uncharge(ctx, bytes); } static void erofs_stream_ctx_destroy(struct erofs_stream_ctx *ctx) { struct erofs_stream_pool_codec *codec; struct erofs_sb_info *sbi; bool cached; uint8_t algorithm; sbi = ctx->sbi; algorithm = ctx->algorithm; cached = ctx->cached; ctx->fini(ctx); KASSERT(ctx->charged_bytes == EROFS_STREAM_CTX_WRAPPER_SIZE, ("erofs stream backend allocation leaked")); uma_zfree(erofs_stream_zone, ctx); mtx_lock(&erofs_stream_lock); codec = &sbi->stream_pool.codec[algorithm]; KASSERT(codec->contexts != 0 && codec->borrowed != 0 && erofs_stream_global_contexts != 0 && erofs_stream_borrowed_contexts != 0, ("erofs stream context accounting underflow")); --codec->contexts; --codec->borrowed; --erofs_stream_global_contexts; --erofs_stream_borrowed_contexts; if (cached) { KASSERT(codec->cached != 0 && erofs_stream_global_cached_by_codec[algorithm] != 0 && erofs_stream_cached_contexts != 0, ("erofs stream cache accounting underflow")); --codec->cached; --erofs_stream_global_cached_by_codec[algorithm]; --erofs_stream_cached_contexts; } erofs_stream_release_locked(sbi, EROFS_STREAM_CTX_WRAPPER_SIZE); ++erofs_stream_destroys; cv_broadcast(&sbi->stream_pool.cv); mtx_unlock(&erofs_stream_lock); } int z_erofs_stream_ctx_get(struct erofs_sb_info *sbi, uint8_t algorithm, size_t context_size, erofs_stream_ctx_init_t *init, erofs_stream_ctx_fini_t *fini, struct erofs_stream_ctx **ctxp) { struct erofs_stream_pool_codec *codec; struct erofs_stream_ctx *ctx; bool cached; int error; *ctxp = NULL; if (algorithm == Z_EROFS_COMPRESSION_LZ4 || algorithm >= Z_EROFS_COMPRESSION_MAX || context_size < sizeof(*ctx) || context_size > EROFS_STREAM_CTX_WRAPPER_SIZE) return (EINVAL); mtx_lock(&erofs_stream_lock); if (!sbi->stream_pool_initialized || sbi->stream_pool.closing) { mtx_unlock(&erofs_stream_lock); return (ENXIO); } codec = &sbi->stream_pool.codec[algorithm]; ctx = STAILQ_FIRST(&codec->idle); if (ctx != NULL) { STAILQ_REMOVE_HEAD(&codec->idle, link); KASSERT(codec->idle_count != 0 && erofs_stream_idle_contexts != 0, ("erofs stream idle accounting underflow")); --codec->idle_count; --erofs_stream_idle_contexts; ++codec->borrowed; ++erofs_stream_borrowed_contexts; ++erofs_stream_reuses; ctx->allocation_failed = false; mtx_unlock(&erofs_stream_lock); *ctxp = ctx; return (0); } cached = codec->cached < (unsigned int)erofs_stream_mount_cached && erofs_stream_global_cached_by_codec[algorithm] < (unsigned int)erofs_stream_global_cached; if (codec->contexts >= (unsigned int)erofs_stream_mount_contexts || erofs_stream_global_contexts >= (unsigned int)erofs_stream_global_contexts_limit || !erofs_stream_reserve_locked(sbi, EROFS_STREAM_CTX_WRAPPER_SIZE)) { ++erofs_stream_exhaustions; mtx_unlock(&erofs_stream_lock); return (ENOMEM); } ++codec->contexts; ++codec->borrowed; ++erofs_stream_global_contexts; ++erofs_stream_borrowed_contexts; if (cached) { ++codec->cached; ++erofs_stream_global_cached_by_codec[algorithm]; ++erofs_stream_cached_contexts; } else { ++erofs_stream_fallbacks; } mtx_unlock(&erofs_stream_lock); ctx = uma_zalloc(erofs_stream_zone, M_NOWAIT | M_ZERO); if (ctx == NULL) { mtx_lock(&erofs_stream_lock); codec = &sbi->stream_pool.codec[algorithm]; --codec->contexts; --codec->borrowed; --erofs_stream_global_contexts; --erofs_stream_borrowed_contexts; if (cached) { --codec->cached; --erofs_stream_global_cached_by_codec[algorithm]; --erofs_stream_cached_contexts; } erofs_stream_release_locked(sbi, EROFS_STREAM_CTX_WRAPPER_SIZE); ++erofs_stream_allocation_failures; cv_broadcast(&sbi->stream_pool.cv); mtx_unlock(&erofs_stream_lock); return (ENOMEM); } ctx->sbi = sbi; ctx->fini = fini; ctx->charged_bytes = EROFS_STREAM_CTX_WRAPPER_SIZE; ctx->algorithm = algorithm; ctx->cached = cached; error = init(ctx); if (error != 0) { erofs_stream_ctx_destroy(ctx); return (error > 0 ? error : EIO); } mtx_lock(&erofs_stream_lock); ++erofs_stream_creations; mtx_unlock(&erofs_stream_lock); *ctxp = ctx; return (0); } void z_erofs_stream_ctx_put(struct erofs_stream_ctx *ctx, bool reusable) { struct erofs_stream_pool_codec *codec; mtx_lock(&erofs_stream_lock); codec = &ctx->sbi->stream_pool.codec[ctx->algorithm]; if (reusable && ctx->cached && !ctx->sbi->stream_pool.closing) { KASSERT(codec->borrowed != 0 && erofs_stream_borrowed_contexts != 0, ("erofs stream borrowed accounting underflow")); --codec->borrowed; --erofs_stream_borrowed_contexts; STAILQ_INSERT_HEAD(&codec->idle, ctx, link); ++codec->idle_count; ++erofs_stream_idle_contexts; cv_broadcast(&ctx->sbi->stream_pool.cv); mtx_unlock(&erofs_stream_lock); return; } mtx_unlock(&erofs_stream_lock); erofs_stream_ctx_destroy(ctx); } static struct erofs_stream_ctx * erofs_stream_take_idle_locked(struct erofs_sb_info *sbi) { struct erofs_stream_pool_codec *codec; struct erofs_stream_ctx *ctx; unsigned int algorithm; mtx_assert(&erofs_stream_lock, MA_OWNED); for (algorithm = 0; algorithm < Z_EROFS_COMPRESSION_MAX; ++algorithm) { codec = &sbi->stream_pool.codec[algorithm]; ctx = STAILQ_FIRST(&codec->idle); if (ctx == NULL) continue; STAILQ_REMOVE_HEAD(&codec->idle, link); KASSERT(codec->idle_count != 0 && erofs_stream_idle_contexts != 0, ("erofs stream idle accounting underflow")); --codec->idle_count; --erofs_stream_idle_contexts; ++codec->borrowed; ++erofs_stream_borrowed_contexts; return (ctx); } return (NULL); } static bool erofs_stream_pool_empty_locked(const struct erofs_stream_pool *pool) { unsigned int algorithm; mtx_assert(&erofs_stream_lock, MA_OWNED); for (algorithm = 0; algorithm < Z_EROFS_COMPRESSION_MAX; ++algorithm) { if (pool->codec[algorithm].contexts != 0) return (false); } return (true); } void z_erofs_stream_pool_init(struct erofs_sb_info *sbi) { unsigned int algorithm; bzero(&sbi->stream_pool, sizeof(sbi->stream_pool)); for (algorithm = 0; algorithm < Z_EROFS_COMPRESSION_MAX; ++algorithm) STAILQ_INIT(&sbi->stream_pool.codec[algorithm].idle); cv_init(&sbi->stream_pool.cv, "erofs stream drain"); mtx_lock(&erofs_stream_lock); sbi->stream_pool_initialized = true; LIST_INSERT_HEAD(&erofs_stream_mounts, sbi, stream_pool_link); mtx_unlock(&erofs_stream_lock); } void z_erofs_stream_pool_fini(struct erofs_sb_info *sbi) { struct erofs_stream_ctx *ctx; if (!sbi->stream_pool_initialized) return; mtx_lock(&erofs_stream_lock); sbi->stream_pool.closing = true; for (;;) { ctx = erofs_stream_take_idle_locked(sbi); if (ctx == NULL) break; mtx_unlock(&erofs_stream_lock); erofs_stream_ctx_destroy(ctx); mtx_lock(&erofs_stream_lock); } while (!erofs_stream_pool_empty_locked(&sbi->stream_pool)) cv_wait(&sbi->stream_pool.cv, &erofs_stream_lock); KASSERT(sbi->stream_pool.resident_bytes == 0, ("erofs stream mount bytes remain at unmount")); LIST_REMOVE(sbi, stream_pool_link); sbi->stream_pool_initialized = false; mtx_unlock(&erofs_stream_lock); cv_destroy(&sbi->stream_pool.cv); } static void erofs_stream_pool_lowmem(void *arg, int howto) { struct erofs_stream_ctx *ctx; struct erofs_sb_info *sbi; (void)arg; (void)howto; for (;;) { ctx = NULL; mtx_lock(&erofs_stream_lock); LIST_FOREACH(sbi, &erofs_stream_mounts, stream_pool_link) { ctx = erofs_stream_take_idle_locked(sbi); if (ctx != NULL) { ++erofs_stream_reclaims; break; } } mtx_unlock(&erofs_stream_lock); if (ctx == NULL) break; erofs_stream_ctx_destroy(ctx); } } static void erofs_stream_pool_global_init(void *arg) { (void)arg; erofs_stream_mount_budget = MIN(erofs_stream_mount_budget, EROFS_STREAM_MOUNT_HARD_BUDGET); erofs_stream_global_budget = MIN(erofs_stream_global_budget, EROFS_STREAM_GLOBAL_HARD_BUDGET); erofs_stream_mount_contexts = MAX(0, MIN(erofs_stream_mount_contexts, (int)EROFS_STREAM_MOUNT_HARD_CONTEXTS)); erofs_stream_global_contexts_limit = MAX(0, MIN( erofs_stream_global_contexts_limit, (int)EROFS_STREAM_GLOBAL_HARD_CONTEXTS)); erofs_stream_mount_cached = MAX(0, MIN(erofs_stream_mount_cached, (int)EROFS_STREAM_MOUNT_CACHED_PER_CODEC)); erofs_stream_global_cached = MAX(0, MIN(erofs_stream_global_cached, (int)EROFS_STREAM_GLOBAL_CACHED_PER_CODEC)); erofs_stream_zone = uma_zcreate("erofs stream ctx", EROFS_STREAM_CTX_WRAPPER_SIZE, NULL, NULL, NULL, NULL, UMA_ALIGN_PTR, 0); KASSERT(erofs_stream_zone != NULL, ("cannot create erofs stream UMA zone")); (void)uma_zone_set_max(erofs_stream_zone, EROFS_STREAM_GLOBAL_HARD_CONTEXTS); uma_zone_set_maxcache(erofs_stream_zone, EROFS_STREAM_GLOBAL_CACHED_PER_CODEC * 3); erofs_stream_lowmem_tag = EVENTHANDLER_REGISTER(vm_lowmem, erofs_stream_pool_lowmem, NULL, LOWMEM_PRI_DEFAULT); } static void erofs_stream_pool_global_fini(void *arg) { (void)arg; if (erofs_stream_lowmem_tag != NULL) EVENTHANDLER_DEREGISTER(vm_lowmem, erofs_stream_lowmem_tag); KASSERT(LIST_EMPTY(&erofs_stream_mounts), ("erofs stream mounts remain at unload")); KASSERT(erofs_stream_global_contexts == 0 && erofs_stream_global_resident == 0 && erofs_stream_borrowed_contexts == 0 && erofs_stream_idle_contexts == 0 && erofs_stream_cached_contexts == 0, ("erofs stream resources remain at unload")); uma_zdestroy(erofs_stream_zone); } SYSINIT(erofs_stream_pool_global, SI_SUB_VFS, SI_ORDER_ANY, erofs_stream_pool_global_init, NULL); SYSUNINIT(erofs_stream_pool_global, SI_SUB_VFS, SI_ORDER_ANY, erofs_stream_pool_global_fini, NULL); static const char *erofs_opts[] = { "export", "from", NULL, }; static vfs_mount_t erofs_mount; static vfs_root_t erofs_root; static vfs_statfs_t erofs_statfs; static vfs_unmount_t erofs_unmount; static vfs_fhtovp_t erofs_fhtovp; #define EROFS_DEVICE_OPT_PREFIX "device." struct erofs_device_arg { uint16_t slot; char *path; }; static int erofs_load_generation_seed(struct erofs_sb_info *sbi, uint32_t sb_size, uint32_t *seedp) { struct erofs_buf buf = EROFS_BUF_INITIALIZER; uint32_t seed; int error; error = erofs_read_metadata(sbi, 0, EROFS_SUPER_OFFSET, sb_size, &buf); if (error != 0) return (error); seed = fnv_32_buf(buf.data, sb_size, FNV1_32_INIT); erofs_put_metabuf(&buf); *seedp = seed != 0 ? seed : 1; return (0); } static void erofs_free_device_args(struct erofs_device_arg *args, unsigned int count) { unsigned int i; if (args == NULL) return; for (i = 0; i < count; ++i) free(args[i].path, M_EROFS); free(args, M_EROFS); } static int erofs_parse_device_slot(const char *name, uint16_t *slotp) { const char *p; unsigned int slot; if (strncmp(name, EROFS_DEVICE_OPT_PREFIX, sizeof(EROFS_DEVICE_OPT_PREFIX) - 1) != 0) return (ENOENT); p = name + sizeof(EROFS_DEVICE_OPT_PREFIX) - 1; if (*p < '1' || *p > '9') return (EINVAL); slot = 0; for (; *p != '\0'; ++p) { if (*p < '0' || *p > '9' || slot > (UINT16_MAX - (*p - '0')) / 10) return (EINVAL); slot = slot * 10 + (*p - '0'); } if (slot == 0 || slot > UINT16_MAX) return (EINVAL); *slotp = slot; return (0); } static int erofs_parse_device_options(struct mount *mp, struct erofs_device_arg **argsp, unsigned int *countp) { struct erofs_device_arg *args; struct vfsopt *opt; char name[32]; unsigned int count, i; uint16_t slot; int error; *argsp = NULL; *countp = 0; count = 0; TAILQ_FOREACH(opt, mp->mnt_optnew, link) { error = erofs_parse_device_slot(opt->name, &slot); if (error == ENOENT) continue; if (error != 0 || opt->value == NULL || opt->len <= 1 || ((char *)opt->value)[opt->len - 1] != '\0') { vfs_mount_error(mp, "erofs: invalid external device option %s", opt->name); return (EINVAL); } if (count == UINT16_MAX) return (E2BIG); ++count; } if (count == 0) return (0); args = mallocarray(count, sizeof(*args), M_EROFS, M_WAITOK | M_ZERO); i = 0; TAILQ_FOREACH(opt, mp->mnt_optnew, link) { error = erofs_parse_device_slot(opt->name, &slot); if (error == ENOENT) continue; KASSERT(error == 0, ("validated EROFS device option changed")); args[i].slot = slot; args[i].path = malloc(opt->len, M_EROFS, M_WAITOK); memcpy(args[i].path, opt->value, opt->len); ++i; } for (i = 0; i < count; ++i) { snprintf(name, sizeof(name), EROFS_DEVICE_OPT_PREFIX "%u", args[i].slot); vfs_deleteopt(mp->mnt_optnew, name); } *argsp = args; *countp = count; return (0); } static void erofs_release_device_info(struct erofs_device_info *dif) { if (dif->cp != NULL) { g_topology_lock(); g_vfs_close(dif->cp); g_topology_unlock(); dif->cp = NULL; } if (dif->devvp != NULL) { vrele(dif->devvp); dif->devvp = NULL; } if (dif->dev != NULL) { dev_rel(dif->dev); dif->dev = NULL; } } static bool erofs_provider_is_duplicate(struct erofs_sb_info *sbi, struct g_provider *pp) { unsigned int i; if (sbi == NULL) return (false); if (sbi->dif0.cp != NULL && sbi->dif0.cp->provider == pp) return (true); for (i = 0; i < sbi->extra_devices; ++i) { if (sbi->devs[i].cp != NULL && sbi->devs[i].cp->provider == pp) return (true); } return (false); } static int erofs_open_device(struct erofs_sb_info *sbi, const char *path, struct erofs_device_info *dif) { struct g_provider *pp; struct nameidata nd; struct vnode *devvp; struct cdev *dev; int error; bzero(dif, sizeof(*dif)); NDINIT(&nd, LOOKUP, FOLLOW | LOCKLEAF, UIO_SYSSPACE, path); error = namei(&nd); if (error != 0) return (error); devvp = nd.ni_vp; NDFREE_PNBUF(&nd); if (!vn_isdisk_error(devvp, &error)) { vput(devvp); return (error); } error = VOP_ACCESS(devvp, VREAD, curthread->td_ucred, curthread); if (error != 0) error = priv_check(curthread, PRIV_VFS_MOUNT_PERM); if (error != 0) { vput(devvp); return (error); } dev = devvp->v_rdev; dev_ref(dev); g_topology_lock(); pp = g_dev_getprovider(dev); if (pp == NULL) error = ENXIO; else if (erofs_provider_is_duplicate(sbi, pp)) error = EINVAL; else error = g_vfs_open(devvp, &dif->cp, "erofs", 0); if (error == 0) { dif->mediasize = dif->cp->provider->mediasize; dif->sectorsize = dif->cp->provider->sectorsize; } g_topology_unlock(); VOP_UNLOCK(devvp); if (error != 0) { dev_rel(dev); vrele(devvp); return (error); } dif->devvp = devvp; dif->dev = dev; if (dif->sectorsize == 0 || (dif->sectorsize & (dif->sectorsize - 1)) != 0) { erofs_release_device_info(dif); return (EINVAL); } return (0); } static void erofs_update_iosize_max(struct mount *mp, const struct erofs_device_info *dif) { u_long iosize; iosize = dif->dev != NULL && dif->dev->si_iosize_max != 0 ? dif->dev->si_iosize_max : MAXPHYS; mp->mnt_iosize_max = MIN(mp->mnt_iosize_max, MIN(iosize, (u_long)MAXPHYS)); } static void erofs_free_dev_context(struct erofs_sb_info *sbi) { unsigned int i; if (sbi->devs != NULL) { for (i = sbi->extra_devices; i > 0; --i) erofs_release_device_info(&sbi->devs[i - 1]); free(sbi->devs, M_EROFS); } } static void erofs_drop_internal_inodes(struct erofs_sb_info *sbi) { if (sbi->metabox_en != NULL) free(sbi->metabox_en, M_EROFS); if (sbi->packed_inode != NULL) free(sbi->packed_inode, M_EROFS); } static void erofs_sb_free(struct erofs_sb_info *sbi) { if (sbi == NULL) return; z_erofs_extent_cache_fini(sbi); z_erofs_stream_pool_fini(sbi); erofs_xattr_prefixes_cleanup(sbi); erofs_drop_internal_inodes(sbi); erofs_free_dev_context(sbi); erofs_release_device_info(&sbi->dif0); free(sbi, M_EROFS); } static int erofs_superblock_csum_verify(struct erofs_sb_info *sbi, const struct erofs_super_block *dsb) { struct erofs_buf buf = EROFS_BUF_INITIALIZER; uint32_t expected, crc; size_t len; int error; if (!erofs_sb_has_sb_chksum(sbi)) return (0); len = 1u << dsb->blkszbits; if (len > EROFS_SUPER_OFFSET) len -= EROFS_SUPER_OFFSET; error = erofs_read_metadata(sbi, 0, EROFS_SUPER_OFFSET, len, &buf); if (error != 0) return (error); crc = calculate_crc32c(erofs_crc32c_seed, (const uint8_t *)buf.data + offsetof(struct erofs_super_block, checksum) + sizeof(dsb->checksum), len - offsetof(struct erofs_super_block, checksum) - sizeof(dsb->checksum)); expected = le32toh(dsb->checksum); erofs_put_metabuf(&buf); if (crc != expected) { vfs_mount_error(sbi->mnt, "erofs: invalid superblock checksum 0x%08x, " "0x%08x expected", crc, expected); return (EINTEGRITY); } return (0); } static void erofs_sb_blocks_root(const struct erofs_super_block *dsb, uint32_t incompat, uint64_t *blocks, erofs_nid_t *root_nid) { *blocks = le32toh(dsb->blocks_lo); if ((incompat & EROFS_FEATURE_INCOMPAT_48BIT) != 0 && dsb->rootnid_8b != 0) { *blocks |= (uint64_t)le16toh(dsb->rb.blocks_hi) << 32; *root_nid = le64toh(dsb->rootnid_8b); } else { *root_nid = le16toh(dsb->rb.rootnid_2b); } } static int erofs_validate_device_size(struct erofs_sb_info *sbi, struct erofs_device_info *dif, erofs_blk_t blocks) { uint64_t bytes; if (blocks == 0) return (EINTEGRITY); if (sbi->block_size < dif->sectorsize || sbi->block_size % dif->sectorsize != 0) return (EINVAL); if (blocks > (UINT64_MAX >> sbi->blkszbits)) return (EINTEGRITY); bytes = blocks << sbi->blkszbits; if (bytes > dif->mediasize) return (ENXIO); return (0); } static int erofs_init_device(struct erofs_sb_info *sbi, struct erofs_device_info *dif, const char *path) { struct erofs_device_info opened; erofs_blk_t blocks, uniaddr; int error; blocks = dif->blocks; uniaddr = dif->uniaddr; error = erofs_open_device(sbi, path, &opened); if (error != 0) return (error); erofs_update_iosize_max(sbi->mnt, &opened); opened.blocks = blocks; opened.uniaddr = uniaddr; *dif = opened; return (erofs_validate_device_size(sbi, dif, dif->blocks)); } static const char * erofs_device_arg_path(const struct erofs_device_arg *args, unsigned int count, unsigned int slot) { unsigned int i; for (i = 0; i < count; ++i) { if (args[i].slot == slot) return (args[i].path); } return (NULL); } static int erofs_scan_devices(struct erofs_sb_info *sbi, const struct erofs_super_block *dsb, const struct erofs_device_arg *args, unsigned int arg_count) { struct erofs_buf buf = EROFS_BUF_INITIALIZER; struct erofs_deviceslot *slots; struct erofs_device_info *dif; const char *path; erofs_off_t devt_off; uint64_t devt_size, image_size, end, other_end; erofs_blk_t maxend; unsigned int i, j, mask; int error; sbi->total_blocks = sbi->dif0.blocks; sbi->flatdev_blocks = sbi->dif0.blocks; if (sbi->extra_devices == 0) { if (arg_count != 0) { vfs_mount_error(sbi->mnt, "erofs: external devices given without a device table"); return (EINVAL); } return (0); } devt_off = (uint64_t)le16toh(dsb->devt_slotoff) * EROFS_DEVT_SLOT_SIZE; devt_size = (uint64_t)sbi->extra_devices * EROFS_DEVT_SLOT_SIZE; if (sbi->dif0.blocks > (UINT64_MAX >> sbi->blkszbits)) return (EINTEGRITY); image_size = sbi->dif0.blocks << sbi->blkszbits; if (devt_off > image_size || devt_size > image_size - devt_off || devt_size > SIZE_MAX) return (EINTEGRITY); error = erofs_read_metadata(sbi, 0, devt_off, (size_t)devt_size, &buf); if (error != 0) return (error); slots = buf.data; sbi->devs = mallocarray(sbi->extra_devices, sizeof(*sbi->devs), M_EROFS, M_WAITOK | M_ZERO); maxend = sbi->dif0.blocks; for (i = 0; i < sbi->extra_devices; ++i) { dif = &sbi->devs[i]; dif->blocks = le32toh(slots[i].blocks_lo); dif->uniaddr = le32toh(slots[i].uniaddr_lo); if (erofs_sb_has_48bit(sbi)) { dif->blocks |= (uint64_t)le16toh(slots[i].blocks_hi) << 32; dif->uniaddr |= (uint64_t)le16toh(slots[i].uniaddr_hi) << 32; } if (dif->blocks == 0 || __builtin_add_overflow(dif->uniaddr, dif->blocks, &end)) { error = EINTEGRITY; goto out; } if (end > (erofs_sb_has_48bit(sbi) ? (1ULL << 48) : (1ULL << 32))) { error = EINTEGRITY; goto out; } if (dif->uniaddr != 0 && dif->uniaddr < sbi->dif0.blocks) { error = EINTEGRITY; goto out; } for (j = 0; j < i; ++j) { if (dif->uniaddr == 0 || sbi->devs[j].uniaddr == 0) continue; if (__builtin_add_overflow(sbi->devs[j].uniaddr, sbi->devs[j].blocks, &other_end)) { error = EINTEGRITY; goto out; } if (dif->uniaddr < other_end && sbi->devs[j].uniaddr < end) { error = EINTEGRITY; goto out; } } if (__builtin_add_overflow(sbi->total_blocks, dif->blocks, &sbi->total_blocks)) { error = EOVERFLOW; goto out; } maxend = MAX(maxend, (erofs_blk_t)end); } erofs_put_metabuf(&buf); sbi->flatdev_blocks = maxend; mask = 1; while (mask < (unsigned int)sbi->extra_devices + 1) mask <<= 1; sbi->device_id_mask = mask - 1; sbi->flatdev = arg_count == 0; if (sbi->flatdev) return (erofs_validate_device_size(sbi, &sbi->dif0, sbi->flatdev_blocks)); if (arg_count != sbi->extra_devices) { vfs_mount_error(sbi->mnt, "erofs: external devices don't match (ondisk %u, given %u)", sbi->extra_devices, arg_count); return (arg_count < sbi->extra_devices ? ENXIO : EINVAL); } for (i = 0; i < arg_count; ++i) { if (args[i].slot == 0 || args[i].slot > sbi->extra_devices) return (EINVAL); } for (i = 0; i < sbi->extra_devices; ++i) { path = erofs_device_arg_path(args, arg_count, i + 1); if (path == NULL) return (ENXIO); error = erofs_init_device(sbi, &sbi->devs[i], path); if (error != 0) return (error); } return (0); out: erofs_put_metabuf(&buf); return (error); } static int erofs_init_packed_inode(struct erofs_sb_info *sbi) { int error; /* Load the packed carrier before any fragment-backed metabox inode. */ if (erofs_sb_has_fragments(sbi) && sbi->packed_nid > 0) { sbi->packed_inode = malloc(sizeof(*sbi->packed_inode), M_EROFS, M_WAITOK | M_ZERO); error = erofs_read_inode(sbi, sbi->packed_nid, sbi->packed_inode); if (error != 0) { free(sbi->packed_inode, M_EROFS); sbi->packed_inode = NULL; return (error); } if (sbi->packed_inode->vtype != VREG || sbi->packed_inode->fragment) { vfs_mount_error(sbi->mnt, "erofs: packed inode nid=%ju is not a non-recursive regular file", (uintmax_t)sbi->packed_nid); return (EINTEGRITY); } } return (0); } static int erofs_init_metabox_inode(struct erofs_sb_info *sbi) { int error; /* * METABOX NIDs address inode slots in this backing inode's data. The * packed carrier is ready first so a compressed metabox may legally end in * a fragment pcluster without reading an uninitialized dependency. */ if (erofs_sb_has_metabox(sbi)) { struct erofs_map_blocks map; sbi->metabox_en = malloc(sizeof(*sbi->metabox_en), M_EROFS, M_WAITOK | M_ZERO); error = erofs_read_inode(sbi, sbi->metabox_nid, sbi->metabox_en); if (error != 0) return (error); if (sbi->metabox_en->vtype != VREG) { vfs_mount_error(sbi->mnt, "erofs: metabox inode nid=%ju is not a regular file", (uintmax_t)sbi->metabox_nid); return (EINTEGRITY); } if (sbi->metabox_en->fragment) { if (sbi->packed_inode == NULL || sbi->packed_inode->nid == sbi->metabox_en->nid || sbi->metabox_en->size == 0) return (EINTEGRITY); map = (struct erofs_map_blocks) { .m_la = sbi->metabox_en->size - 1, }; error = erofs_map_blocks(sbi, sbi->metabox_en, &map); if (error != 0 || (map.m_flags & EROFS_MAP_FRAGMENT) == 0) return (error != 0 ? error : EINTEGRITY); } } return (0); } static int erofs_read_superblock(struct erofs_sb_info *sbi, struct erofs_super_block *dsb) { struct erofs_buf buf = EROFS_BUF_INITIALIZER; uint32_t unsupported; int error; error = erofs_read_metadata(sbi, 0, EROFS_SUPER_OFFSET, sizeof(*dsb), &buf); if (error != 0) return (error); memcpy(dsb, buf.data, sizeof(*dsb)); erofs_put_metabuf(&buf); if (le32toh(dsb->magic) != EROFS_SUPER_MAGIC_V1) return (EINVAL); if (dsb->blkszbits < 9 || dsb->blkszbits > PAGE_SHIFT) return (EINVAL); sbi->blkszbits = dsb->blkszbits; sbi->block_size = 1u << sbi->blkszbits; sbi->feature_compat = le32toh(dsb->feature_compat); error = erofs_superblock_csum_verify(sbi, dsb); if (error != 0) return (error); if (dsb->dirblkbits != 0) return (EOPNOTSUPP); sbi->feature_incompat = le32toh(dsb->feature_incompat); sbi->packed_nid = le64toh(dsb->packed_nid); sbi->extra_devices = erofs_sb_has_device_table(sbi) ? le16toh(dsb->extra_devices) : 0; unsupported = sbi->feature_incompat & ~EROFS_ALL_FEATURE_INCOMPAT; if (unsupported != 0) return (EOPNOTSUPP); sbi->sb_size = 128 + dsb->sb_extslots * EROFS_SB_EXTSLOT_SIZE; if (sbi->sb_size > PAGE_SIZE - EROFS_SUPER_OFFSET) return (EINVAL); sbi->meta_blkaddr = le32toh(dsb->meta_blkaddr); sbi->xattr_blkaddr = le32toh(dsb->xattr_blkaddr); sbi->xattr_prefix_start = le32toh(dsb->xattr_prefix_start); sbi->xattr_prefix_count = dsb->xattr_prefix_count; if (erofs_sb_has_ishare_xattrs(sbi) && dsb->ishare_xattr_prefix_id >= sbi->xattr_prefix_count) return (EINTEGRITY); /* Preserve the raw feature declaration and gate its format at use sites. */ sbi->xattr_filter_reserved = dsb->xattr_filter_reserved; erofs_sb_blocks_root(dsb, sbi->feature_incompat, &sbi->blocks, &sbi->root_nid); sbi->dif0.blocks = sbi->blocks; error = erofs_validate_device_size(sbi, &sbi->dif0, sbi->dif0.blocks); if (error != 0) return (error); sbi->inos = le64toh(dsb->inos); sbi->epoch = (int64_t)le64toh(dsb->epoch); sbi->fixed_nsec = le32toh(dsb->fixed_nsec); if (sbi->fixed_nsec >= 1000000000) return (EINTEGRITY); error = erofs_load_generation_seed(sbi, sbi->sb_size, &sbi->generation_seed); if (error != 0) return (error); if (sbi->packed_nid != 0 && erofs_nid_in_metabox(sbi->packed_nid)) return (EINTEGRITY); if (erofs_sb_has_metabox(sbi)) { if (sbi->sb_size <= offsetof(struct erofs_super_block, metabox_nid)) return (EINTEGRITY); sbi->metabox_nid = le64toh(dsb->metabox_nid); if (erofs_nid_in_metabox(sbi->metabox_nid)) return (EINTEGRITY); } return (z_erofs_parse_cfgs(sbi, dsb)); } static int erofs_mountfs(struct erofs_device_info *primary, struct mount *mp, const struct erofs_device_arg *args, unsigned int arg_count) { struct erofs_sb_info *sbi; struct erofs_inode root; struct erofs_super_block dsb; int error; sbi = malloc(sizeof(*sbi), M_EROFS, M_WAITOK | M_ZERO); sbi->mnt = mp; z_erofs_extent_cache_init(sbi); z_erofs_stream_pool_init(sbi); sbi->dif0 = *primary; bzero(primary, sizeof(*primary)); error = erofs_read_superblock(sbi, &dsb); if (error != 0) goto fail; error = erofs_scan_devices(sbi, &dsb, args, arg_count); if (error != 0) goto fail; if (erofs_sb_has_shared_ea_in_metabox(sbi) && !erofs_sb_has_metabox(sbi)) { error = EINTEGRITY; goto fail; } error = erofs_init_packed_inode(sbi); if (error != 0) goto fail; error = erofs_init_metabox_inode(sbi); if (error != 0) goto fail; error = erofs_read_inode(sbi, sbi->root_nid, &root); if (error != 0) goto fail; if (root.vtype != VDIR) { vfs_mount_error(mp, "erofs: root inode nid=%ju is not a directory", (uintmax_t)sbi->root_nid); error = EINTEGRITY; goto fail; } error = erofs_xattr_prefixes_init(sbi); if (error != 0) goto fail; set_opt(&sbi->opt, POSIX_ACL); memcpy(sbi->volume_name, dsb.volume_name, 16); sbi->volume_name[16] = '\0'; mp->mnt_data = sbi; mp->mnt_stat.f_fsid.val[0] = dev2udev(sbi->dif0.devvp->v_rdev); mp->mnt_stat.f_fsid.val[1] = mp->mnt_vfc->vfc_typenum; MNT_ILOCK(mp); mp->mnt_flag |= MNT_LOCAL | MNT_RDONLY | MNT_ACLS; mp->mnt_kern_flag |= MNTK_LOOKUP_SHARED | MNTK_EXTENDED_SHARED | MNTK_USES_BCACHE; MNT_IUNLOCK(mp); return (0); fail: erofs_sb_free(sbi); return (error); } static int erofs_mount(struct mount *mp) { struct erofs_device_arg *args; struct erofs_device_info primary; char *fspec; unsigned int arg_count; int error, len; MNT_ILOCK(mp); mp->mnt_flag |= MNT_RDONLY; MNT_IUNLOCK(mp); if (mp->mnt_flag & MNT_UPDATE) { if (vfs_flagopt(mp->mnt_optnew, "export", NULL, 0)) return (0); return (EOPNOTSUPP); } args = NULL; arg_count = 0; error = erofs_parse_device_options(mp, &args, &arg_count); if (error != 0) return (error); if (vfs_filteropt(mp->mnt_optnew, erofs_opts) != 0) { error = EINVAL; goto out_args; } fspec = NULL; error = vfs_getopt(mp->mnt_optnew, "from", (void **)&fspec, &len); if (error != 0 || fspec == NULL || len == 0 || fspec[len - 1] != '\0') { error = EINVAL; goto out_args; } mp->mnt_iosize_max = MAXPHYS; error = erofs_open_device(NULL, fspec, &primary); if (error != 0) goto out_args; erofs_update_iosize_max(mp, &primary); error = erofs_mountfs(&primary, mp, args, arg_count); out_args: erofs_free_device_args(args, arg_count); if (error != 0) return (error); vfs_mountedfrom(mp, fspec); return (erofs_statfs(mp, &mp->mnt_stat)); } static int erofs_root(struct mount *mp, int flags, struct vnode **vpp) { int error; error = erofs_vget(mp, MTOE(mp)->root_nid, flags, vpp); if (error != 0) vfs_mount_error(mp, "erofs: failed to load root nid %ju: error %d", (uintmax_t)MTOE(mp)->root_nid, error); return (error); } static int erofs_statfs(struct mount *mp, struct statfs *sbp) { struct erofs_sb_info *sbi; sbi = MTOE(mp); sbp->f_bsize = sbi->block_size; sbp->f_iosize = sbi->block_size; sbp->f_blocks = sbi->total_blocks; sbp->f_bfree = 0; sbp->f_bavail = 0; sbp->f_files = sbi->inos; sbp->f_ffree = 0; return (0); } static int erofs_unmount(struct mount *mp, int mntflags) { struct erofs_sb_info *sbi; int error, flags; flags = ((mntflags & MNT_FORCE) != 0) ? FORCECLOSE : 0; error = vflush(mp, 0, flags, curthread); if (error != 0) return (error); sbi = MTOE(mp); mp->mnt_data = NULL; erofs_sb_free(sbi); return (0); } /* Persistent EROFS file handle to locked vnode. */ static int erofs_fhtovp(struct mount *mp, struct fid *fhp, int flags, struct vnode **vpp) { struct erofs_fid efid; struct erofs_inode *vi; struct vnode *vp; erofs_nid_t nid; int error; *vpp = NULL; bzero(&efid, sizeof(efid)); memcpy(&efid, fhp, sizeof(efid)); if (efid.len != sizeof(efid) || efid.pad != 0) return (EINVAL); nid = ((uint64_t)efid.nid_hi << 32) | efid.nid_lo; if (!erofs_nid_is_valid(MTOE(mp), nid)) return (ESTALE); error = VFS_VGET(mp, (ino_t)nid, flags, &vp); if (error != 0) return (error); vi = VTOE(vp); if (vi->mode == 0 || vi->nlink == 0 || vi->nid != nid || vi->generation != efid.gen) { vput(vp); return (ESTALE); } *vpp = vp; return (0); } static struct vfsops erofs_vfsops = { .vfs_fhtovp = erofs_fhtovp, .vfs_mount = erofs_mount, .vfs_root = erofs_root, .vfs_statfs = erofs_statfs, .vfs_unmount = erofs_unmount, .vfs_vget = erofs_vget, }; VFS_SET(erofs_vfsops, erofs, VFCF_READONLY); MODULE_DEPEND(erofs, acl_posix1e, 1, 1, 1); MODULE_DEPEND(erofs, zlib, 1, 1, 1); MODULE_VERSION(erofs, 1);