/* SPDX-License-Identifier: GPL-2.0-or-later */ /* FreeBSD per-mount management controls for EROFS. */ #include #include #include #include #include #include #include #include "compress.h" #include "internal.h" #define EROFS_DIR_RA_DEFAULT_BYTES (16UL * 1024) #define EROFS_DIR_RA_HARD_MAX_BYTES (1024UL * 1024) #define EROFS_DIR_RA_SLOTS 256UL enum erofs_cache_metric { EROFS_CACHE_HITS, EROFS_CACHE_MISSES, EROFS_CACHE_BYPASSES, EROFS_CACHE_EVICTIONS, EROFS_CACHE_RECLAIMS, EROFS_CACHE_WAITER_MERGES, EROFS_CACHE_ENTRIES, EROFS_CACHE_BYTES, }; static struct mtx erofs_control_id_lock; static uint64_t erofs_control_next_id = 1; MTX_SYSINIT(erofs_control_id, &erofs_control_id_lock, "erofs control id", MTX_DEF); SYSCTL_NODE(_vfs, OID_AUTO, erofs, CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "EROFS filesystem"); SYSCTL_NODE(_vfs_erofs, OID_AUTO, mounts, CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Mounted EROFS filesystems"); void erofs_management_init(struct erofs_sb_info *sbi) { mtx_init(&sbi->control_lock, "erofs control", NULL, MTX_DEF); sbi->dir_ra_bytes = EROFS_DIR_RA_DEFAULT_BYTES; sbi->dir_ra_max_bytes = EROFS_DIR_RA_HARD_MAX_BYTES; sbi->control_initialized = true; } void erofs_management_fini(struct erofs_sb_info *sbi) { if (!sbi->control_initialized) return; KASSERT(!sbi->sysctl_registered, ("erofs management sysctls remain registered")); mtx_destroy(&sbi->control_lock); sbi->control_initialized = false; } uint64_t erofs_dir_ra_bytes(struct erofs_sb_info *sbi) { uint64_t value; mtx_lock(&sbi->control_lock); value = sbi->dir_ra_bytes; mtx_unlock(&sbi->control_lock); return (value); } /* The readahead is limited by the size of the per-directory run lists. */ static uint64_t erofs_dir_ra_limit(const struct erofs_sb_info *sbi) { return (MIN((uint64_t)EROFS_DIR_RA_HARD_MAX_BYTES, (uint64_t)sbi->block_size * EROFS_DIR_RA_SLOTS)); } int erofs_set_dir_ra_bytes(struct erofs_sb_info *sbi, uint64_t value) { if (sbi->block_size == 0 || value > erofs_dir_ra_limit(sbi) || (value != 0 && value % sbi->block_size != 0)) return (EINVAL); mtx_lock(&sbi->control_lock); if (sbi->control_closing) { mtx_unlock(&sbi->control_lock); return (ENXIO); } sbi->dir_ra_bytes = value; mtx_unlock(&sbi->control_lock); return (0); } void erofs_record_dir_readahead(struct erofs_sb_info *sbi, unsigned int blocks) { mtx_lock(&sbi->control_lock); ++sbi->dir_ra_requests; sbi->dir_ra_blocks += blocks; mtx_unlock(&sbi->control_lock); } static int erofs_parse_uint64(const char *value, size_t len, uint64_t *result) { size_t i; uint64_t parsed; if (value == NULL || len <= 1 || value[len - 1] != '\0' || strnlen(value, len) != len - 1) return (EINVAL); parsed = 0; for (i = 0; i < len - 1; ++i) { if (value[i] < '0' || value[i] > '9') return (EINVAL); if (parsed > (UINT64_MAX - (value[i] - '0')) / 10) return (ERANGE); parsed = parsed * 10 + (value[i] - '0'); } *result = parsed; return (0); } int erofs_parse_dir_ra_value(struct erofs_sb_info *sbi, struct vfsoptlist *opts, uint64_t *valuep, bool *presentp) { struct vfsopt *opt; uint64_t value; int error; *presentp = false; TAILQ_FOREACH(opt, opts, link) { if (strcmp(opt->name, "dir_ra_bytes") != 0) continue; error = erofs_parse_uint64(opt->value, opt->len, &value); if (error != 0) return (error); if (sbi->block_size == 0 || value > erofs_dir_ra_limit(sbi) || (value != 0 && value % sbi->block_size != 0)) return (EINVAL); *valuep = value; *presentp = true; return (0); } return (0); } int erofs_parse_dir_ra_option(struct erofs_sb_info *sbi, struct vfsoptlist *opts) { uint64_t value; bool present; int error; error = erofs_parse_dir_ra_value(sbi, opts, &value, &present); if (error != 0 || !present) return (error); return (erofs_set_dir_ra_bytes(sbi, value)); } static int erofs_sysctl_dir_ra(SYSCTL_HANDLER_ARGS) { struct erofs_sb_info *sbi; uint64_t value; int error; sbi = arg1; value = erofs_dir_ra_bytes(sbi); error = sysctl_handle_64(oidp, &value, 0, req); if (error != 0 || req->newptr == NULL) return (error); return (erofs_set_dir_ra_bytes(sbi, value)); } static int erofs_sysctl_dir_ra_metric(SYSCTL_HANDLER_ARGS) { struct erofs_sb_info *sbi; uint64_t value; sbi = arg1; mtx_lock(&sbi->control_lock); value = arg2 == 0 ? sbi->dir_ra_requests : sbi->dir_ra_blocks; mtx_unlock(&sbi->control_lock); return (sysctl_handle_64(oidp, &value, 0, req)); } #if WITH_EROFS_ZIP static int erofs_validate_zip_value(const char *value, int len) { if (value == NULL || len <= 1 || value[len - 1] != '\0' || strnlen(value, (size_t)len) != (size_t)len - 1) return (EINVAL); return (0); } static int erofs_parse_zip_uint(const char *value, int len, unsigned int *result) { size_t i; unsigned int parsed; if (erofs_validate_zip_value(value, len) != 0) return (EINVAL); parsed = 0; for (i = 0; i < (size_t)len - 1; ++i) { if (value[i] < '0' || value[i] > '9' || parsed > (UINT_MAX - (value[i] - '0')) / 10) return (EINVAL); parsed = parsed * 10 + (value[i] - '0'); } *result = parsed; return (0); } int erofs_parse_zip_options(struct mount *mp, struct erofs_sb_info_opts *opts, unsigned int *syncp, bool *cache_presentp, bool *sync_presentp) { struct vfsopt *opt; unsigned int value; bool queue_priority_present; int error; *cache_presentp = false; *sync_presentp = false; queue_priority_present = false; TAILQ_FOREACH(opt, mp->mnt_optnew, link) { if (strcmp(opt->name, "cache_strategy") == 0) { if (opt->value == NULL || opt->len <= 1 || ((char *)opt->value)[opt->len - 1] != '\0' || strnlen(opt->value, opt->len) != (size_t)opt->len - 1) return (EINVAL); if (strcmp(opt->value, "disabled") == 0) opts->cache_strategy = EROFS_ZIP_CACHE_DISABLED; else if (strcmp(opt->value, "readahead") == 0) opts->cache_strategy = EROFS_ZIP_CACHE_READAHEAD; else if (strcmp(opt->value, "readaround") == 0) opts->cache_strategy = EROFS_ZIP_CACHE_READAROUND; else return (EINVAL); *cache_presentp = true; continue; } if (strcmp(opt->name, "worker_queue_priority") == 0) { error = erofs_parse_zip_uint(opt->value, opt->len, &value); if (error != 0 || value > UINT8_MAX) return (EINVAL); opts->worker_queue_priority = (uint8_t)value; queue_priority_present = true; continue; } if (strcmp(opt->name, "sync_decompress") != 0) continue; error = erofs_validate_zip_value(opt->value, opt->len); if (error != 0) return (error); if (strcmp(opt->value, "auto") == 0) value = EROFS_SYNC_DECOMPRESS_AUTO; else if (strcmp(opt->value, "on") == 0) value = EROFS_SYNC_DECOMPRESS_FORCE_ON; else if (strcmp(opt->value, "off") == 0) value = EROFS_SYNC_DECOMPRESS_FORCE_OFF; else { error = erofs_parse_zip_uint(opt->value, opt->len, &value); if (error != 0 || value > EROFS_SYNC_DECOMPRESS_FORCE_OFF) return (EINVAL); } *syncp = value; *sync_presentp = true; } if (*cache_presentp) vfs_deleteopt(mp->mnt_optnew, "cache_strategy"); if (*sync_presentp) vfs_deleteopt(mp->mnt_optnew, "sync_decompress"); if (queue_priority_present) vfs_deleteopt(mp->mnt_optnew, "worker_queue_priority"); return (0); } static int erofs_sysctl_drop_caches(SYSCTL_HANDLER_ARGS) { struct erofs_sb_info *sbi; uint64_t value; int error; if (req->newptr == NULL) return (EPERM); sbi = arg1; value = 0; error = sysctl_handle_64(oidp, &value, 0, req); if (error != 0) return (error); if (value != 1) return (EINVAL); return (z_erofs_extent_cache_drop(sbi)); } static int erofs_sysctl_cache_metric(SYSCTL_HANDLER_ARGS) { struct erofs_cache_stats stats; struct erofs_sb_info *sbi; uint64_t value; sbi = arg1; z_erofs_extent_cache_stats(sbi, &stats); switch (arg2) { case EROFS_CACHE_HITS: value = stats.hits; break; case EROFS_CACHE_MISSES: value = stats.misses; break; case EROFS_CACHE_BYPASSES: value = stats.bypasses; break; case EROFS_CACHE_EVICTIONS: value = stats.evictions; break; case EROFS_CACHE_RECLAIMS: value = stats.reclaims; break; case EROFS_CACHE_WAITER_MERGES: value = stats.waiter_merges; break; case EROFS_CACHE_ENTRIES: value = stats.entries; break; case EROFS_CACHE_BYTES: value = stats.bytes; break; default: return (EINVAL); } return (sysctl_handle_64(oidp, &value, 0, req)); } static int erofs_sysctl_zip_setting(SYSCTL_HANDLER_ARGS) { struct erofs_sb_info *sbi; int value; int error; sbi = arg1; mtx_lock(&sbi->control_lock); value = arg2 == 0 ? (int)sbi->sync_decompress : (int)sbi->opt.cache_strategy; mtx_unlock(&sbi->control_lock); error = sysctl_handle_int(oidp, &value, 0, req); if (error != 0 || req->newptr == NULL) return (error); if (arg2 == 0) { if (value < EROFS_SYNC_DECOMPRESS_AUTO || value > EROFS_SYNC_DECOMPRESS_FORCE_OFF) return (EINVAL); } else { if (value < EROFS_ZIP_CACHE_DISABLED || value > EROFS_ZIP_CACHE_READAROUND) return (EINVAL); } mtx_lock(&sbi->control_lock); if (sbi->control_closing) { mtx_unlock(&sbi->control_lock); return (ENXIO); } if (arg2 == 0) sbi->sync_decompress = (unsigned int)value; else sbi->opt.cache_strategy = (uint8_t)value; mtx_unlock(&sbi->control_lock); return (0); } static int erofs_sysctl_worker_queue_priority(SYSCTL_HANDLER_ARGS) { struct erofs_sb_info *sbi; int error, value; sbi = arg1; mtx_lock(&sbi->control_lock); value = sbi->decompress_queue_priority; mtx_unlock(&sbi->control_lock); error = sysctl_handle_int(oidp, &value, 0, req); if (error != 0 || req->newptr == NULL) return (error); if (value < 0 || value > UINT8_MAX) return (EINVAL); mtx_lock(&sbi->control_lock); if (sbi->control_closing) { mtx_unlock(&sbi->control_lock); return (ENXIO); } sbi->decompress_queue_priority = (uint8_t)value; sbi->opt.worker_queue_priority = (uint8_t)value; mtx_unlock(&sbi->control_lock); return (0); } #endif int erofs_register_sysfs(struct erofs_sb_info *sbi) { struct sysctl_oid *cache_node, *mount_node; char name[24]; int error; mtx_lock(&erofs_control_id_lock); sbi->control_id = erofs_control_next_id++; mtx_unlock(&erofs_control_id_lock); sbi->dir_ra_max_bytes = erofs_dir_ra_limit(sbi); snprintf(name, sizeof(name), "%ju", (uintmax_t)sbi->control_id); sysctl_ctx_init(&sbi->sysctl_ctx); sbi->sysctl_registered = true; mount_node = SYSCTL_ADD_NODE(&sbi->sysctl_ctx, SYSCTL_STATIC_CHILDREN(_vfs_erofs_mounts), OID_AUTO, name, CTLFLAG_RD | CTLFLAG_MPSAFE, NULL, "EROFS mount"); if (mount_node == NULL) goto fail; if (SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "mount_id", CTLFLAG_RD, &sbi->control_id, 0, "Unique mount control identifier") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "dir_ra_bytes", CTLTYPE_U64 | CTLFLAG_RW | CTLFLAG_MPSAFE, sbi, 0, erofs_sysctl_dir_ra, "QU", "Directory readahead window in bytes") == NULL || SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "dir_ra_max_bytes", CTLFLAG_RD, &sbi->dir_ra_max_bytes, 0, "Maximum directory readahead window") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "dir_ra_requests", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, 0, erofs_sysctl_dir_ra_metric, "QU", "Directory readahead submissions") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "dir_ra_blocks", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, 1, erofs_sysctl_dir_ra_metric, "QU", "Directory readahead blocks submitted") == NULL) goto fail; #if WITH_EROFS_ZIP if (SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "drop_caches", CTLTYPE_U64 | CTLFLAG_WR | CTLFLAG_MPSAFE, sbi, 0, erofs_sysctl_drop_caches, "QU", "Write 1 to drop the FreeBSD decoded extent cache") == NULL) goto fail; if (SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "sync_decompress", CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_MPSAFE, sbi, 0, erofs_sysctl_zip_setting, "I", "0=auto, 1=force synchronous, 2=taskqueue fallback") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "cache_strategy", CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_MPSAFE, sbi, 1, erofs_sysctl_zip_setting, "I", "0=disabled, 1=readahead, 2=readaround") == NULL) goto fail; cache_node = SYSCTL_ADD_NODE(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node), OID_AUTO, "decoded_cache", CTLFLAG_RD | CTLFLAG_MPSAFE, NULL, "FreeBSD decoded extent cache, not Linux managed pclusters"); if (cache_node == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "hits", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_HITS, erofs_sysctl_cache_metric, "QU", "Cache hits") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "misses", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_MISSES, erofs_sysctl_cache_metric, "QU", "Cache misses") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "bypasses", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_BYPASSES, erofs_sysctl_cache_metric, "QU", "Cache admission bypasses") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "evictions", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_EVICTIONS, erofs_sysctl_cache_metric, "QU", "Cache evictions") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "reclaims", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_RECLAIMS, erofs_sysctl_cache_metric, "QU", "Cache pressure reclaims") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "waiter_merges", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_WAITER_MERGES, erofs_sysctl_cache_metric, "QU", "Inflight cache waiter merges") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "entries", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_ENTRIES, erofs_sysctl_cache_metric, "QU", "Resident decoded extents") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "bytes", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi, EROFS_CACHE_BYTES, erofs_sysctl_cache_metric, "QU", "Resident decoded bytes") == NULL || SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "worker_submissions", CTLFLAG_RD, &sbi->decompress_submissions, 0, "Taskqueue decompression submissions") == NULL || SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "worker_runs", CTLFLAG_RD, &sbi->decompress_worker_runs, 0, "Taskqueue decompression worker runs") == NULL || SYSCTL_ADD_UINT(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "worker_count", CTLFLAG_RD, &sbi->decompress_worker_count, 0, "Bounded decompression worker count") == NULL || SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "queue_fallbacks", CTLFLAG_RD, &sbi->decompress_queue_fallbacks, 0, "Direct decode after taskqueue fallback") == NULL || SYSCTL_ADD_U8(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "worker_cpu_bound", CTLFLAG_RD, (uint8_t *)&sbi->decompress_worker_cpu_bound, 0, "Workers restricted to the online CPU set") == NULL || SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node), OID_AUTO, "worker_queue_priority", CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_MPSAFE, sbi, 0, erofs_sysctl_worker_queue_priority, "I", "Taskqueue order priority; 0 is highest and 255 is lowest") == NULL) goto fail; #else (void)cache_node; #endif return (0); fail: error = erofs_unregister_sysfs(sbi); return (error != 0 ? error : ENOMEM); } int erofs_unregister_sysfs(struct erofs_sb_info *sbi) { int error; if (!sbi->sysctl_registered) return (0); mtx_lock(&sbi->control_lock); sbi->control_closing = true; mtx_unlock(&sbi->control_lock); error = sysctl_ctx_free(&sbi->sysctl_ctx); if (error != 0) { mtx_lock(&sbi->control_lock); sbi->control_closing = false; mtx_unlock(&sbi->control_lock); return (error); } sbi->sysctl_registered = false; return (0); }