1248 lines
42 KiB
Bash
1248 lines
42 KiB
Bash
#!/bin/sh
|
|
set -eu
|
|
|
|
: "${PRE15_DUT:?PRE15_DUT is required}"
|
|
: "${PRE15_ROOT:?PRE15_ROOT is required}"
|
|
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
|
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
|
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
|
. "$PRE15_LIB_DIR/runner.sh"
|
|
|
|
case_script=$PRE15_DUT/tests/pre15/cases/B29-context-pool.sh
|
|
kld_builder=$PRE15_DUT/tests/pre15/fixtures/B28-build-kld.sh
|
|
old_gate=$PRE15_DUT/tests/pre15/gates/P15-076.sh
|
|
old_input=$PRE15_DUT/tests/pre15/gates/P15-076-input.json
|
|
old_evidence=$PRE15_ROOT/planning/pre15/evidence/20260815T171051Z-G05-P15-076
|
|
checkpoint=$PRE15_ROOT/planning/pre15/evidence/20260817T153426Z-B29/checkpoint.md
|
|
artifacts=$PRE15_RUN_DIR/artifacts
|
|
fixtures=$PRE15_CASE_TMP/fixtures
|
|
action=${1:-runtime}
|
|
|
|
case "$action" in
|
|
runtime|k2) ;;
|
|
*) pre15_fail_usage "unknown B29 action: $action" ;;
|
|
esac
|
|
|
|
for tool in awk cc cmp file fsck.erofs git mkfs.erofs nm python3 sha256sum \
|
|
tar timeout; do
|
|
command -v "$tool" >/dev/null 2>&1 || \
|
|
pre15_infra_blocked "missing B29 tool: $tool"
|
|
done
|
|
for fixture in "$case_script" "$kld_builder" "$old_gate" "$old_input" \
|
|
"$old_evidence/gate-output/benchmark-results.json" \
|
|
"$old_evidence/gate-output/state-model-result.json" \
|
|
"$old_evidence/VERDICT.md" "$checkpoint"; do
|
|
pre15_record_fixture "b29-$(basename "$fixture")" "$fixture"
|
|
done
|
|
for source in internal.h super.c decompressor_lzma.c decompressor_deflate.c \
|
|
decompressor_zstd.c; do
|
|
pre15_record_fixture "b29-$source" "$PRE15_DUT/src/$source"
|
|
done
|
|
for source in decompressor_lzma.c decompressor_deflate.c \
|
|
decompressor_zstd.c; do
|
|
pre15_record_fixture "b29-linux-$source" "$PRE15_ROOT/src-linux/$source"
|
|
done
|
|
mkdir -p "$artifacts"
|
|
|
|
if test "$action" = k2; then
|
|
for config in 0 1; do
|
|
module=$PRE15_CASE_TMP/B29-zstdio$config.ko
|
|
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
|
"$PRE15_FREEBSD_SRC" "$module" \
|
|
"$PRE15_CASE_TMP/kld-work-$config" "$config" \
|
|
>"$artifacts/B29-build-zstdio$config.stdout" \
|
|
2>"$artifacts/B29-build-zstdio$config.stderr"; then
|
|
pre15_dut_fail "B29 zstdio$config exact-ABI KLD build failed"
|
|
fi
|
|
file "$module" >"$artifacts/B29-zstdio$config-file.txt"
|
|
sha256sum "$module" >"$artifacts/B29-zstdio$config-sha256.txt"
|
|
nm -g "$module" | LC_ALL=C sort \
|
|
>"$artifacts/B29-zstdio$config-nm-global.txt"
|
|
nm -u "$module" | LC_ALL=C sort \
|
|
>"$artifacts/B29-zstdio$config-nm-u.txt"
|
|
done
|
|
awk '$1 != "U" { print $NF }' "$artifacts/B29-zstdio0-nm-global.txt" \
|
|
>"$PRE15_CASE_TMP/B29-zstdio0-global-names.txt"
|
|
awk '$1 != "U" { print $NF }' "$artifacts/B29-zstdio1-nm-global.txt" \
|
|
>"$PRE15_CASE_TMP/B29-zstdio1-global-names.txt"
|
|
if ! diff -u "$PRE15_CASE_TMP/B29-zstdio0-global-names.txt" \
|
|
"$PRE15_CASE_TMP/B29-zstdio1-global-names.txt" \
|
|
>"$artifacts/B29-config-nm-global.diff"; then
|
|
pre15_dut_fail 'B29 defined globals differ by ZSTDIO configuration'
|
|
fi
|
|
awk '$NF ~ /^ZSTD_/ { print $NF }' "$artifacts/B29-zstdio1-nm-u.txt" \
|
|
>"$PRE15_CASE_TMP/B29-zstd-symbols.txt"
|
|
printf '%s\n' ZSTD_DCtx_setParameter ZSTD_createDCtx_advanced \
|
|
ZSTD_decompressStream ZSTD_freeDCtx ZSTD_isError \
|
|
>"$PRE15_CASE_TMP/B29-zstd-expected.txt"
|
|
if ! cmp "$PRE15_CASE_TMP/B29-zstd-expected.txt" \
|
|
"$PRE15_CASE_TMP/B29-zstd-symbols.txt"; then
|
|
pre15_dut_fail 'B29 changed the accepted five-symbol Zstd provider ABI'
|
|
fi
|
|
pre15_target_reached
|
|
printf '%s\n' \
|
|
'B29 FreeBSD 15 exact-ABI K2 PASS' \
|
|
'zstdio0/zstdio1 compile, link, global-symbol parity, and five-symbol optional Zstd ABI PASS'
|
|
exit 0
|
|
fi
|
|
|
|
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" \
|
|
"$old_evidence" "$artifacts/B29-source-audit.json" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
root = Path(sys.argv[1])
|
|
dut = Path(sys.argv[2])
|
|
freebsd = Path(sys.argv[3])
|
|
evidence = Path(sys.argv[4])
|
|
report_path = Path(sys.argv[5])
|
|
|
|
|
|
def fail(message: str) -> None:
|
|
raise SystemExit(message)
|
|
|
|
|
|
current = {
|
|
name: (dut / "src" / name).read_text(encoding="utf-8")
|
|
for name in (
|
|
"internal.h",
|
|
"super.c",
|
|
"decompressor_lzma.c",
|
|
"decompressor_deflate.c",
|
|
"decompressor_zstd.c",
|
|
)
|
|
}
|
|
linux = {
|
|
name: (root / "src-linux" / name).read_text(encoding="utf-8")
|
|
for name in (
|
|
"decompressor_lzma.c",
|
|
"decompressor_deflate.c",
|
|
"decompressor_zstd.c",
|
|
)
|
|
}
|
|
benchmark = json.loads(
|
|
(evidence / "gate-output/benchmark-results.json").read_text(encoding="ascii")
|
|
)
|
|
old_state = json.loads(
|
|
(evidence / "gate-output/state-model-result.json").read_text(encoding="ascii")
|
|
)
|
|
old_verdict = (evidence / "VERDICT.md").read_text(encoding="utf-8")
|
|
|
|
assertions = 0
|
|
|
|
|
|
def require(condition: bool, message: str) -> None:
|
|
global assertions
|
|
assertions += 1
|
|
if not condition:
|
|
fail(message)
|
|
|
|
|
|
for codec, record in benchmark.items():
|
|
require(record["baseline_context_allocator_share_percent"] >= 10.0,
|
|
f"{codec} old allocator-share benefit regressed")
|
|
require(record["allocator_event_reduction_percent"] >= 25.0,
|
|
f"{codec} old lifecycle benefit regressed")
|
|
require(old_state["pool_exhaustion_fallback"] == "fresh baseline allocation",
|
|
"historical unbounded fallback record changed")
|
|
require("global-exhaustion scenario" in old_verdict,
|
|
"historical global-exhaustion rejection is no longer explicit")
|
|
require("maximum window log 16" in old_verdict and "8 MiB" in old_verdict,
|
|
"historical maximum-parameter gap is no longer explicit")
|
|
|
|
super_source = current["super.c"]
|
|
hard_caps = {
|
|
"EROFS_STREAM_MOUNT_HARD_BUDGET": "32UL * 1024 * 1024",
|
|
"EROFS_STREAM_GLOBAL_HARD_BUDGET": "128UL * 1024 * 1024",
|
|
"EROFS_STREAM_ALLOCATION_HARD_MAX": "4UL * 1024 * 1024",
|
|
"EROFS_STREAM_MOUNT_HARD_CONTEXTS": "16U",
|
|
"EROFS_STREAM_GLOBAL_HARD_CONTEXTS": "64U",
|
|
"EROFS_STREAM_MOUNT_CACHED_PER_CODEC": "2U",
|
|
"EROFS_STREAM_GLOBAL_CACHED_PER_CODEC": "16U",
|
|
}
|
|
for name, value in hard_caps.items():
|
|
require(f"#define {name} ({value})" in super_source,
|
|
f"missing production hard cap {name}")
|
|
for token in (
|
|
'TUNABLE_ULONG("vfs.erofs.stream_pool.mount_budget"',
|
|
'TUNABLE_ULONG("vfs.erofs.stream_pool.global_budget"',
|
|
'TUNABLE_INT("vfs.erofs.stream_pool.global_contexts"',
|
|
"MIN(erofs_stream_mount_budget,",
|
|
"MIN(erofs_stream_global_budget,",
|
|
"uma_zone_set_max(erofs_stream_zone,",
|
|
"erofs_stream_reserve_locked(sbi, EROFS_STREAM_CTX_WRAPPER_SIZE)",
|
|
"++erofs_stream_fallbacks",
|
|
"++erofs_stream_exhaustions",
|
|
"EVENTHANDLER_REGISTER(vm_lowmem",
|
|
"while (!erofs_stream_pool_empty_locked(&sbi->stream_pool))",
|
|
"KASSERT(erofs_stream_global_contexts == 0",
|
|
):
|
|
require(token in super_source, f"missing bounded pool token: {token}")
|
|
require(super_source.find("erofs_stream_reserve_locked(sbi, EROFS_STREAM_CTX_WRAPPER_SIZE)") <
|
|
super_source.find("uma_zalloc(erofs_stream_zone"),
|
|
"wrapper allocation occurs before hard-budget reservation")
|
|
require("fresh baseline allocation" not in super_source,
|
|
"production retained the historical unbounded fallback")
|
|
|
|
internal = current["internal.h"]
|
|
for field in ("resident_bytes", "contexts", "cached", "borrowed", "idle_count",
|
|
"closing", "stream_pool_initialized"):
|
|
require(re.search(rf"\b{field}\b", internal) is not None,
|
|
f"pool state omits {field}")
|
|
|
|
lzma = current["decompressor_lzma.c"]
|
|
deflate = current["decompressor_deflate.c"]
|
|
zstd = current["decompressor_zstd.c"]
|
|
case_source = (dut / "tests/pre15/cases/B29-context-pool.sh").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
require("xz_dec_microlzma_alloc(XZ_SINGLE" in lzma,
|
|
"FreeBSD LZMA no longer uses the bounded XZ_SINGLE mode")
|
|
require("ctx->state_bytes = sizeof(*ctx->state);" in lzma,
|
|
"LZMA fixed state is not charged exactly")
|
|
require("dict_size > Z_EROFS_LZMA_MAX_DICT_SIZE" in lzma,
|
|
"LZMA 8 MiB format hard limit is absent")
|
|
require("M_WAITOK" not in lzma,
|
|
"LZMA backend still has an unbounded waiting allocation")
|
|
require("z_erofs_stream_ctx_alloc(pool, bytes)" in deflate,
|
|
"Deflate backend allocations bypass hard accounting")
|
|
require("inflateReset2(strm" in deflate,
|
|
"Deflate reusable context is not reset")
|
|
require("z_erofs_stream_ctx_alloc(opaque, size)" in zstd,
|
|
"Zstd backend allocations bypass hard accounting")
|
|
require("zstd->windowlog > 10" in zstd and
|
|
"rq->sbi->zstd_windowlog + 10 > 20" in zstd,
|
|
"Zstd maximum window parameter is not enforced")
|
|
require("ZSTD_DCtx_reset" not in zstd,
|
|
"B29 added a new Zstd provider ABI symbol")
|
|
require("error == 0 && !rq->partial_decoding" in zstd,
|
|
"partial Zstd state is incorrectly cached without reset")
|
|
require('"$B29_BAD_MOUNT" 45' in case_source,
|
|
"B29 runtime uses the Linux EOPNOTSUPP value instead of FreeBSD 45")
|
|
require("run_exhaustion_phase lzma byte" in case_source and
|
|
"run_exhaustion_phase zstd byte" in case_source and
|
|
"run_exhaustion_phase lzma count" in case_source,
|
|
"B29 runtime omits a production LZMA/Zstd byte or global count gate")
|
|
for source in (deflate, zstd):
|
|
require("M_WAITOK" not in source,
|
|
"codec backend retained an unbounded waiting allocation")
|
|
|
|
xz_source = (freebsd / "sys/contrib/xz-embedded/linux/lib/xz/xz_dec_lzma2.c").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
xz_private = (freebsd / "sys/contrib/xz-embedded/linux/lib/xz/xz_private.h").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
require("if (DEC_IS_MULTI(mode))" in xz_source and
|
|
"s->s.dict.buf = vmalloc(dict_size);" in xz_source,
|
|
"FreeBSD MicroLZMA allocation branch changed")
|
|
require("#\tdefine DEC_IS_MULTI(mode) ((mode) != XZ_SINGLE)" in xz_private,
|
|
"FreeBSD XZ_SINGLE dictionary-allocation proof changed")
|
|
|
|
for codec, tokens in {
|
|
"lzma": ("get an available lzma context", "push back LZMA stream context"),
|
|
"deflate": ("get an available DEFLATE context", "push back DEFLATE stream context"),
|
|
"zstd": ("get an available ZSTD context", "push back ZSTD stream context"),
|
|
}.items():
|
|
for token in tokens:
|
|
require(token in linux[f"decompressor_{codec}.c"],
|
|
f"Linux reusable-stream intent changed for {codec}")
|
|
|
|
status = subprocess.run(
|
|
["git", "-C", str(root), "status", "--porcelain"],
|
|
check=True, text=True, stdout=subprocess.PIPE,
|
|
).stdout.splitlines()
|
|
allowed_implementation = {
|
|
"repo-pre-15/src/decompressor_lzma.c",
|
|
"repo-pre-15/src/decompressor_deflate.c",
|
|
"repo-pre-15/src/decompressor_zstd.c",
|
|
"repo-pre-15/src/internal.h",
|
|
"repo-pre-15/src/super.c",
|
|
"repo-pre-15/tests/pre15/cases/B29-context-pool.sh",
|
|
}
|
|
for line in status:
|
|
path = line[3:]
|
|
require(path in allowed_implementation or path.startswith("planning/pre15/evidence/20260817T153426Z-B29/") or
|
|
path in {
|
|
"planning/pre15/execution-status.md",
|
|
"planning/pre15/final-review/closure.tsv",
|
|
"planning/pre15/final-review/completion-audit.md",
|
|
"planning/pre15/final-review/completion-audit.tsv",
|
|
}, f"B29 dirty path escapes allowed scope: {path}")
|
|
|
|
report = {
|
|
"assertions": assertions,
|
|
"batch": "B29",
|
|
"benefit_gate_retained": True,
|
|
"freebsd_difference": {
|
|
"allocator": "UMA wrapper plus exact mount/global byte charging",
|
|
"lzma": "XZ_SINGLE fixed state; no dictionary buffer allocation",
|
|
"locking": "one FreeBSD mutex, no Linux waitqueue/per-CPU API copy",
|
|
},
|
|
"global_exhaustion_required": True,
|
|
"hard_caps": hard_caps,
|
|
"linux_reusable_stream_intent": True,
|
|
"status": "PASS",
|
|
}
|
|
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
|
print(json.dumps(report, indent=2, sort_keys=True))
|
|
PY
|
|
then
|
|
pre15_dut_fail 'B29 benefit/source/hard-cap audit failed'
|
|
fi
|
|
|
|
cat >"$PRE15_CASE_TMP/B29-pool-oracle.c" <<'EOF'
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#define WRAPPER_BYTES 256U
|
|
#define MOUNT_CACHED 2U
|
|
#define GLOBAL_CACHED 16U
|
|
|
|
struct budget {
|
|
pthread_mutex_t lock;
|
|
pthread_cond_t cv;
|
|
size_t bytes;
|
|
size_t byte_limit;
|
|
unsigned contexts;
|
|
unsigned context_limit;
|
|
unsigned cached;
|
|
unsigned borrowed;
|
|
unsigned exhaustions;
|
|
};
|
|
|
|
struct mount_pool {
|
|
struct budget *global;
|
|
size_t bytes;
|
|
size_t byte_limit;
|
|
unsigned contexts;
|
|
unsigned context_limit;
|
|
unsigned cached;
|
|
unsigned borrowed;
|
|
unsigned idle;
|
|
bool closing;
|
|
};
|
|
|
|
struct context {
|
|
struct mount_pool *mount;
|
|
bool cached;
|
|
};
|
|
|
|
static unsigned assertions;
|
|
|
|
static void
|
|
check(bool condition, const char *message)
|
|
{
|
|
++assertions;
|
|
if (!condition) {
|
|
fprintf(stderr, "B29 oracle failure: %s\n", message);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static void
|
|
budget_init(struct budget *global, size_t bytes, unsigned contexts)
|
|
{
|
|
memset(global, 0, sizeof(*global));
|
|
pthread_mutex_init(&global->lock, NULL);
|
|
pthread_cond_init(&global->cv, NULL);
|
|
global->byte_limit = bytes;
|
|
global->context_limit = contexts;
|
|
}
|
|
|
|
static void
|
|
budget_fini(struct budget *global)
|
|
{
|
|
pthread_cond_destroy(&global->cv);
|
|
pthread_mutex_destroy(&global->lock);
|
|
}
|
|
|
|
static void
|
|
mount_init(struct mount_pool *mount, struct budget *global, size_t bytes,
|
|
unsigned contexts)
|
|
{
|
|
memset(mount, 0, sizeof(*mount));
|
|
mount->global = global;
|
|
mount->byte_limit = bytes;
|
|
mount->context_limit = contexts;
|
|
}
|
|
|
|
static int
|
|
acquire(struct mount_pool *mount, bool inject_failure, struct context **out)
|
|
{
|
|
struct budget *global = mount->global;
|
|
struct context *ctx;
|
|
bool cached;
|
|
|
|
*out = NULL;
|
|
pthread_mutex_lock(&global->lock);
|
|
if (mount->closing) {
|
|
pthread_mutex_unlock(&global->lock);
|
|
return ENXIO;
|
|
}
|
|
if (mount->idle != 0) {
|
|
--mount->idle;
|
|
++mount->borrowed;
|
|
++global->borrowed;
|
|
pthread_mutex_unlock(&global->lock);
|
|
ctx = calloc(1, sizeof(*ctx));
|
|
check(ctx != NULL, "idle wrapper allocation");
|
|
ctx->mount = mount;
|
|
ctx->cached = true;
|
|
*out = ctx;
|
|
return 0;
|
|
}
|
|
cached = mount->cached < MOUNT_CACHED && global->cached < GLOBAL_CACHED;
|
|
if (mount->contexts >= mount->context_limit ||
|
|
global->contexts >= global->context_limit ||
|
|
mount->bytes > mount->byte_limit - WRAPPER_BYTES ||
|
|
global->bytes > global->byte_limit - WRAPPER_BYTES) {
|
|
++global->exhaustions;
|
|
pthread_mutex_unlock(&global->lock);
|
|
return ENOMEM;
|
|
}
|
|
mount->bytes += WRAPPER_BYTES;
|
|
global->bytes += WRAPPER_BYTES;
|
|
++mount->contexts;
|
|
++global->contexts;
|
|
++mount->borrowed;
|
|
++global->borrowed;
|
|
if (cached) {
|
|
++mount->cached;
|
|
++global->cached;
|
|
}
|
|
pthread_mutex_unlock(&global->lock);
|
|
if (inject_failure) {
|
|
pthread_mutex_lock(&global->lock);
|
|
mount->bytes -= WRAPPER_BYTES;
|
|
global->bytes -= WRAPPER_BYTES;
|
|
--mount->contexts;
|
|
--global->contexts;
|
|
--mount->borrowed;
|
|
--global->borrowed;
|
|
if (cached) {
|
|
--mount->cached;
|
|
--global->cached;
|
|
}
|
|
pthread_cond_broadcast(&global->cv);
|
|
pthread_mutex_unlock(&global->lock);
|
|
return ENOMEM;
|
|
}
|
|
ctx = calloc(1, sizeof(*ctx));
|
|
check(ctx != NULL, "new wrapper allocation");
|
|
ctx->mount = mount;
|
|
ctx->cached = cached;
|
|
*out = ctx;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
release(struct context *ctx, bool reusable)
|
|
{
|
|
struct mount_pool *mount = ctx->mount;
|
|
struct budget *global = mount->global;
|
|
|
|
pthread_mutex_lock(&global->lock);
|
|
if (reusable && ctx->cached && !mount->closing) {
|
|
--mount->borrowed;
|
|
--global->borrowed;
|
|
++mount->idle;
|
|
pthread_cond_broadcast(&global->cv);
|
|
pthread_mutex_unlock(&global->lock);
|
|
free(ctx);
|
|
return;
|
|
}
|
|
--mount->borrowed;
|
|
--global->borrowed;
|
|
--mount->contexts;
|
|
--global->contexts;
|
|
mount->bytes -= WRAPPER_BYTES;
|
|
global->bytes -= WRAPPER_BYTES;
|
|
if (ctx->cached) {
|
|
--mount->cached;
|
|
--global->cached;
|
|
}
|
|
pthread_cond_broadcast(&global->cv);
|
|
pthread_mutex_unlock(&global->lock);
|
|
free(ctx);
|
|
}
|
|
|
|
static void
|
|
drain(struct mount_pool *mount)
|
|
{
|
|
struct budget *global = mount->global;
|
|
|
|
pthread_mutex_lock(&global->lock);
|
|
mount->closing = true;
|
|
while (mount->idle != 0) {
|
|
--mount->idle;
|
|
--mount->contexts;
|
|
--global->contexts;
|
|
--mount->cached;
|
|
--global->cached;
|
|
mount->bytes -= WRAPPER_BYTES;
|
|
global->bytes -= WRAPPER_BYTES;
|
|
}
|
|
while (mount->contexts != 0)
|
|
pthread_cond_wait(&global->cv, &global->lock);
|
|
pthread_mutex_unlock(&global->lock);
|
|
}
|
|
|
|
struct acquire_result {
|
|
struct mount_pool *mount;
|
|
int error;
|
|
};
|
|
|
|
static void *
|
|
acquire_worker(void *opaque)
|
|
{
|
|
struct acquire_result *result = opaque;
|
|
struct context *ctx;
|
|
|
|
result->error = acquire(result->mount, false, &ctx);
|
|
if (result->error == 0)
|
|
release(ctx, false);
|
|
return NULL;
|
|
}
|
|
|
|
struct delayed_release {
|
|
struct context *ctx;
|
|
};
|
|
|
|
static void *
|
|
release_worker(void *opaque)
|
|
{
|
|
struct delayed_release *delayed = opaque;
|
|
struct timespec delay = { .tv_nsec = 50000000 };
|
|
|
|
nanosleep(&delay, NULL);
|
|
release(delayed->ctx, true);
|
|
return NULL;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
struct acquire_result results[2];
|
|
struct delayed_release delayed;
|
|
struct mount_pool a, b, c;
|
|
struct budget global;
|
|
struct context *one, *two, *three;
|
|
pthread_t threads[2];
|
|
unsigned before;
|
|
|
|
budget_init(&global, 4096, 8);
|
|
mount_init(&a, &global, 4096, 8);
|
|
check(acquire(&a, false, &one) == 0, "first cached acquire");
|
|
release(one, true);
|
|
check(a.idle == 1 && a.cached == 1 && global.contexts == 1,
|
|
"first release did not cache");
|
|
check(acquire(&a, false, &one) == 0, "idle reuse acquire");
|
|
check(a.idle == 0 && a.borrowed == 1, "idle reuse counters");
|
|
release(one, true);
|
|
drain(&a);
|
|
check(global.contexts == 0 && global.bytes == 0, "reuse drain leaked");
|
|
budget_fini(&global);
|
|
|
|
budget_init(&global, 4096, 8);
|
|
mount_init(&a, &global, 4096, 8);
|
|
check(acquire(&a, false, &one) == 0, "fallback first acquire");
|
|
check(acquire(&a, false, &two) == 0, "fallback second acquire");
|
|
check(acquire(&a, false, &three) == 0, "bounded fallback acquire");
|
|
check(one->cached && two->cached && !three->cached,
|
|
"cache saturation did not select bounded temporary context");
|
|
release(one, true);
|
|
release(two, true);
|
|
release(three, true);
|
|
check(a.contexts == 2 && a.idle == 2 && global.contexts == 2,
|
|
"temporary fallback was retained or cached contexts were lost");
|
|
drain(&a);
|
|
check(global.contexts == 0 && global.bytes == 0, "fallback drain leaked");
|
|
budget_fini(&global);
|
|
|
|
budget_init(&global, 4096, 1);
|
|
mount_init(&a, &global, 4096, 8);
|
|
mount_init(&b, &global, 4096, 8);
|
|
mount_init(&c, &global, 4096, 8);
|
|
check(acquire(&a, false, &one) == 0, "global owner acquire");
|
|
release(one, true);
|
|
before = global.exhaustions;
|
|
results[0].mount = &b;
|
|
results[1].mount = &c;
|
|
check(pthread_create(&threads[0], NULL, acquire_worker, &results[0]) == 0,
|
|
"first exhaustion worker create");
|
|
check(pthread_create(&threads[1], NULL, acquire_worker, &results[1]) == 0,
|
|
"second exhaustion worker create");
|
|
check(pthread_join(threads[0], NULL) == 0, "first exhaustion worker join");
|
|
check(pthread_join(threads[1], NULL) == 0, "second exhaustion worker join");
|
|
check(results[0].error == ENOMEM && results[1].error == ENOMEM,
|
|
"concurrent global exhaustion errno is unstable");
|
|
check(global.exhaustions == before + 2 && global.contexts == 1 &&
|
|
global.bytes == WRAPPER_BYTES,
|
|
"global exhaustion changed retained ownership");
|
|
drain(&a);
|
|
check(acquire(&b, false, &one) == 0, "recovery acquire failed");
|
|
release(one, true);
|
|
drain(&b);
|
|
drain(&c);
|
|
check(global.contexts == 0 && global.bytes == 0 && global.borrowed == 0,
|
|
"global recovery leaked resources");
|
|
budget_fini(&global);
|
|
|
|
budget_init(&global, 511, 8);
|
|
mount_init(&a, &global, 511, 8);
|
|
mount_init(&b, &global, 511, 8);
|
|
check(acquire(&a, false, &one) == 0, "byte owner acquire");
|
|
check(acquire(&b, false, &two) == ENOMEM,
|
|
"global byte exhaustion did not return ENOMEM");
|
|
release(one, false);
|
|
check(acquire(&b, false, &two) == 0, "byte recovery acquire failed");
|
|
release(two, false);
|
|
drain(&a);
|
|
drain(&b);
|
|
check(global.bytes == 0 && global.contexts == 0, "byte recovery leaked");
|
|
budget_fini(&global);
|
|
|
|
budget_init(&global, 4096, 8);
|
|
mount_init(&a, &global, 4096, 8);
|
|
check(acquire(&a, true, &one) == ENOMEM,
|
|
"construction failure did not return ENOMEM");
|
|
check(global.bytes == 0 && global.contexts == 0 && global.borrowed == 0,
|
|
"construction failure did not roll back reservations");
|
|
drain(&a);
|
|
budget_fini(&global);
|
|
|
|
budget_init(&global, 4096, 8);
|
|
mount_init(&a, &global, 4096, 8);
|
|
check(acquire(&a, false, &one) == 0, "drain borrower acquire");
|
|
delayed.ctx = one;
|
|
check(pthread_create(&threads[0], NULL, release_worker, &delayed) == 0,
|
|
"drain release worker create");
|
|
drain(&a);
|
|
check(pthread_join(threads[0], NULL) == 0, "drain release worker join");
|
|
check(global.bytes == 0 && global.contexts == 0 && global.borrowed == 0,
|
|
"unmount drain did not wait for borrower cleanup");
|
|
budget_fini(&global);
|
|
|
|
printf("B29 pool oracle PASS assertions=%u global-count global-bytes failure rollback recovery drain\n",
|
|
assertions);
|
|
return 0;
|
|
}
|
|
EOF
|
|
|
|
if ! cc -std=c11 -Wall -Wextra -Werror -pthread \
|
|
"$PRE15_CASE_TMP/B29-pool-oracle.c" -o "$PRE15_CASE_TMP/B29-pool-oracle" \
|
|
>"$artifacts/B29-pool-oracle-build.stdout" \
|
|
2>"$artifacts/B29-pool-oracle-build.stderr" || \
|
|
! "$PRE15_CASE_TMP/B29-pool-oracle" \
|
|
>"$artifacts/B29-pool-oracle.stdout" \
|
|
2>"$artifacts/B29-pool-oracle.stderr"; then
|
|
pre15_dut_fail 'B29 concurrent pool accounting oracle failed'
|
|
fi
|
|
|
|
if ! python3 -B - "$fixtures" "$artifacts/B29-fixture-index.json" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
output = Path(sys.argv[1])
|
|
index_path = Path(sys.argv[2])
|
|
source = output / "source"
|
|
images = output / "images"
|
|
output.mkdir(parents=True)
|
|
source.mkdir()
|
|
images.mkdir()
|
|
|
|
|
|
def run(argv: list[str]) -> None:
|
|
completed = subprocess.run(
|
|
argv, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
text=True, timeout=120,
|
|
)
|
|
if completed.returncode != 0:
|
|
raise SystemExit(f"command failed ({completed.returncode}): {' '.join(argv)}\n{completed.stdout}")
|
|
|
|
|
|
block = bytes((index * 29 + 17) & 0xFF for index in range(4096))
|
|
for index in range(3):
|
|
payload = (block[index * 257 :] + block[: index * 257]) * 64
|
|
(source / f"payload-{index}.bin").write_bytes(payload)
|
|
|
|
specs = {
|
|
"lzma": "-zlzma,level=6,dictsize=65536",
|
|
"lzma-max": "-zlzma,level=6,dictsize=8388608",
|
|
"deflate": "-zdeflate,level=1,dictsize=32768",
|
|
"zstd": "-zzstd,level=3,dictsize=65536",
|
|
"zstd-max": "-zzstd,level=3,dictsize=1048576",
|
|
}
|
|
records = []
|
|
for name, option in specs.items():
|
|
image = images / f"{name}.erofs"
|
|
run([
|
|
"mkfs.erofs", "-T0", "--all-time", "--force-uid=0", "--force-gid=0",
|
|
"-C65536", "--max-extent-bytes=1048576", option, str(image), str(source),
|
|
])
|
|
run(["fsck.erofs", "--extract=" + str(output / (name + "-extract")), str(image)])
|
|
for index in range(3):
|
|
actual = output / (name + "-extract") / f"payload-{index}.bin"
|
|
if actual.read_bytes() != (source / f"payload-{index}.bin").read_bytes():
|
|
raise SystemExit(f"{name} extraction mismatch for payload-{index}")
|
|
records.append({"name": name, "sha256": hashlib.sha256(image.read_bytes()).hexdigest()})
|
|
|
|
|
|
def crc32c(data: bytes | bytearray, seed: int = 0xFFFFFFFF) -> int:
|
|
value = seed
|
|
polynomial = 0x82F63B78
|
|
for byte in data:
|
|
value ^= byte
|
|
for _ in range(8):
|
|
value = (value >> 1) ^ (polynomial if value & 1 else 0)
|
|
return value & 0xFFFFFFFF
|
|
|
|
|
|
def mutate(source_image: Path, target: int, mutate) -> Path:
|
|
data = bytearray(source_image.read_bytes())
|
|
super_offset = 1024
|
|
block_size = 1 << data[super_offset + 12]
|
|
sb_size = 128 + data[super_offset + 13] * 16
|
|
algorithms = struct.unpack_from("<H", data, super_offset + 84)[0]
|
|
offset = super_offset + sb_size
|
|
found = False
|
|
for algorithm in range(4):
|
|
if algorithms & (1 << algorithm) == 0:
|
|
continue
|
|
offset = (offset + 3) & ~3
|
|
length = struct.unpack_from("<H", data, offset)[0]
|
|
size = length if length else 65536
|
|
payload = offset + 2
|
|
if algorithm == target:
|
|
mutate(data, payload, size)
|
|
found = True
|
|
break
|
|
offset = payload + size
|
|
if not found:
|
|
raise SystemExit(f"algorithm {target} config not found in {source_image}")
|
|
feature_compat = struct.unpack_from("<I", data, super_offset + 8)[0]
|
|
if feature_compat & 1:
|
|
checksum_size = block_size
|
|
if checksum_size > super_offset:
|
|
checksum_size -= super_offset
|
|
checksum = crc32c(
|
|
data[super_offset + 8 : super_offset + checksum_size],
|
|
seed=0x5045B54A,
|
|
)
|
|
struct.pack_into("<I", data, super_offset + 4, checksum)
|
|
destination = images / (source_image.stem + "-over.erofs")
|
|
destination.write_bytes(data)
|
|
return destination
|
|
|
|
|
|
lzma_over = mutate(images / "lzma-max.erofs", 1,
|
|
lambda data, offset, size: struct.pack_into("<I", data, offset, 8388609))
|
|
zstd_over = mutate(images / "zstd-max.erofs", 3,
|
|
lambda data, offset, size: data.__setitem__(offset + 1, 11))
|
|
records.extend([
|
|
{"name": "lzma-over", "sha256": hashlib.sha256(lzma_over.read_bytes()).hexdigest()},
|
|
{"name": "zstd-over", "sha256": hashlib.sha256(zstd_over.read_bytes()).hexdigest()},
|
|
])
|
|
|
|
for extracted in output.glob("*-extract"):
|
|
shutil.rmtree(extracted)
|
|
|
|
index_path.write_text(json.dumps({
|
|
"files_per_image": 3,
|
|
"images": records,
|
|
"lzma_max_dict": 8388608,
|
|
"schema": 1,
|
|
"zstd_max_window": 1048576,
|
|
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
|
print(json.dumps({"images": len(records), "status": "PASS"}, sort_keys=True))
|
|
PY
|
|
then
|
|
pre15_runner_fail 'B29 real EROFS fixture generation failed'
|
|
fi
|
|
|
|
sha256sum "$artifacts/B29-source-audit.json" \
|
|
"$artifacts/B29-fixture-index.json" \
|
|
"$artifacts/B29-pool-oracle.stdout" >"$artifacts/SHA256SUMS"
|
|
|
|
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
|
|
pre15_target_reached
|
|
elif test "${PRE15_MODE:-host}" != qemu; then
|
|
pre15_target_reached
|
|
printf '%s\n' \
|
|
'B29 focused host/static gate PASS' \
|
|
"$(cat "$artifacts/B29-pool-oracle.stdout")" \
|
|
'Old G05 benefit retained; production hard caps, bounded fallback, true global count/byte exhaustion model, rollback, drain, and recovery PASS' \
|
|
'QEMU runtime NOT_RUN in host mode; full feature suite NOT_RUN'
|
|
exit 0
|
|
fi
|
|
|
|
for tool in scp ssh; do
|
|
command -v "$tool" >/dev/null 2>&1 || \
|
|
pre15_infra_blocked "missing B29 QEMU tool: $tool"
|
|
done
|
|
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
|
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
|
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
|
|
|
module=$PRE15_CASE_TMP/B29-erofs-zstdio1.ko
|
|
fixture_archive=$PRE15_CASE_TMP/B29-fixtures.tar.gz
|
|
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
|
"$PRE15_CASE_TMP/kld-work-qemu" 1 >"$artifacts/B29-kld-build.stdout" \
|
|
2>"$artifacts/B29-kld-build.stderr"; then
|
|
pre15_dut_fail 'B29 exact-ABI zstdio1 QEMU KLD build failed'
|
|
fi
|
|
pre15_record_module "$module"
|
|
file "$module" >"$artifacts/B29-kld-file.txt"
|
|
sha256sum "$module" >"$artifacts/B29-kld-sha256.txt"
|
|
nm -u "$module" | LC_ALL=C sort >"$artifacts/B29-kld-nm-u.txt"
|
|
tar -C "$fixtures" -czf "$fixture_archive" images source
|
|
|
|
cat >"$PRE15_CASE_TMP/B29-read-probe.c" <<'EOF'
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
static int
|
|
read_all(const char *path, unsigned char **datap, size_t *sizep, int *errorp)
|
|
{
|
|
unsigned char *data;
|
|
size_t capacity, size;
|
|
ssize_t count;
|
|
int fd;
|
|
|
|
fd = open(path, O_RDONLY);
|
|
if (fd < 0) {
|
|
*errorp = errno;
|
|
return (-1);
|
|
}
|
|
capacity = 1024 * 1024;
|
|
data = malloc(capacity);
|
|
if (data == NULL)
|
|
return (-1);
|
|
size = 0;
|
|
while ((count = read(fd, data + size, capacity - size)) > 0) {
|
|
size += (size_t)count;
|
|
if (size == capacity)
|
|
break;
|
|
}
|
|
if (count < 0) {
|
|
*errorp = errno;
|
|
close(fd);
|
|
free(data);
|
|
return (-1);
|
|
}
|
|
close(fd);
|
|
*datap = data;
|
|
*sizep = size;
|
|
*errorp = 0;
|
|
return (0);
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
unsigned char *actual, *expected;
|
|
size_t actual_size, expected_size;
|
|
int actual_error, expected_error;
|
|
|
|
if (argc == 4 && strcmp(argv[1], "pass") == 0) {
|
|
if (read_all(argv[2], &actual, &actual_size, &actual_error) != 0 ||
|
|
read_all(argv[3], &expected, &expected_size, &expected_error) != 0)
|
|
return (1);
|
|
if (actual_size != expected_size ||
|
|
memcmp(actual, expected, actual_size) != 0)
|
|
return (1);
|
|
free(actual);
|
|
free(expected);
|
|
return (0);
|
|
}
|
|
if (argc == 4 && strcmp(argv[1], "errno") == 0) {
|
|
expected_error = atoi(argv[3]);
|
|
if (read_all(argv[2], &actual, &actual_size, &actual_error) == 0) {
|
|
free(actual);
|
|
return (1);
|
|
}
|
|
return (actual_error == expected_error ? 0 : 1);
|
|
}
|
|
fprintf(stderr, "usage: B29-read-probe pass ACTUAL EXPECTED | errno PATH ERRNO\n");
|
|
return (2);
|
|
}
|
|
EOF
|
|
|
|
cat >"$PRE15_CASE_TMP/B29-mount-probe.c" <<'EOF'
|
|
#include <sys/mount.h>
|
|
#include <sys/uio.h>
|
|
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static void
|
|
add_iovec(struct iovec **iov, int *iovlen, const char *name, const char *value)
|
|
{
|
|
*iov = realloc(*iov, sizeof(**iov) * (size_t)(*iovlen + 2));
|
|
(*iov)[*iovlen].iov_base = __DECONST(char *, name);
|
|
(*iov)[*iovlen].iov_len = strlen(name) + 1;
|
|
++*iovlen;
|
|
(*iov)[*iovlen].iov_base = __DECONST(char *, value);
|
|
(*iov)[*iovlen].iov_len = strlen(value) + 1;
|
|
++*iovlen;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
struct iovec *iov;
|
|
int error, expected, iovlen;
|
|
|
|
if (argc != 4)
|
|
return (2);
|
|
expected = atoi(argv[3]);
|
|
iov = NULL;
|
|
iovlen = 0;
|
|
add_iovec(&iov, &iovlen, "fstype", "erofs");
|
|
add_iovec(&iov, &iovlen, "fspath", argv[2]);
|
|
add_iovec(&iov, &iovlen, "from", argv[1]);
|
|
if (nmount(iov, (unsigned)iovlen, MNT_RDONLY) == 0) {
|
|
unmount(argv[2], 0);
|
|
free(iov);
|
|
return (1);
|
|
}
|
|
error = errno;
|
|
free(iov);
|
|
return (error == expected ? 0 : 1);
|
|
}
|
|
EOF
|
|
|
|
pre15_scp()
|
|
{
|
|
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
|
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
|
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
|
-P "$PRE15_QEMU_SSH_PORT" \
|
|
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
|
|
}
|
|
|
|
pre15_guest_ssh_bounded()
|
|
{
|
|
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
|
|
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
|
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
|
-p "$PRE15_QEMU_SSH_PORT" \
|
|
"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
|
|
}
|
|
|
|
if ! pre15_scp "$fixture_archive" /root/B29-fixtures.tar.gz || \
|
|
! pre15_scp "$module" /root/B29-erofs-zstdio1.ko || \
|
|
! pre15_scp "$PRE15_CASE_TMP/B29-read-probe.c" /root/B29-read-probe.c || \
|
|
! pre15_scp "$PRE15_CASE_TMP/B29-mount-probe.c" /root/B29-mount-probe.c; then
|
|
pre15_infra_blocked 'could not transfer B29 module, fixtures, or probes'
|
|
fi
|
|
if ! pre15_guest_ssh_bounded \
|
|
'rm -rf /root/B29-fixtures && mkdir /root/B29-fixtures && tar -xzf /root/B29-fixtures.tar.gz -C /root/B29-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B29-read-probe /root/B29-read-probe.c && cc -O2 -Wall -Wextra -Werror -o /root/B29-mount-probe /root/B29-mount-probe.c'; then
|
|
pre15_infra_blocked 'could not prepare B29 guest fixtures/probes'
|
|
fi
|
|
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
|
|
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
|
fi
|
|
pre15_guest_ssh_bounded \
|
|
'kenv -u vfs.erofs.stream_pool.global_budget >/dev/null 2>&1 || true; kenv -u vfs.erofs.stream_pool.global_contexts >/dev/null 2>&1 || true; kenv -u vfs.erofs.stream_pool.global_cached_per_codec >/dev/null 2>&1 || true'
|
|
if ! pre15_guest_ssh_bounded kldload /root/B29-erofs-zstdio1.ko \
|
|
>"$artifacts/B29-kldload.stdout" 2>"$artifacts/B29-kldload.stderr"; then
|
|
if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B29-kldload.stderr"; then
|
|
pre15_infra_blocked 'guest lacks the exact B29 KLD provider ABI'
|
|
fi
|
|
pre15_dut_fail 'B29 exact-ABI KLD failed to load'
|
|
fi
|
|
pre15_own_guest_kld B29-erofs-zstdio1.ko 'B29 exact-source KLD'
|
|
|
|
attach_mount()
|
|
{
|
|
image=$1
|
|
label=$2
|
|
B29_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
|
-f "/root/B29-fixtures/images/$image") || \
|
|
pre15_dut_fail "$label md attach failed"
|
|
case "$B29_MD" in
|
|
md[0-9]*) ;;
|
|
*) pre15_runner_fail "unexpected B29 md unit: $B29_MD" ;;
|
|
esac
|
|
pre15_own_guest_md "$B29_MD" "$label md"
|
|
B29_MOUNT=/mnt/pre15-b29-$label
|
|
pre15_guest_ssh_bounded mkdir -p "$B29_MOUNT"
|
|
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B29_MD" \
|
|
"$B29_MOUNT" || pre15_dut_fail "$label mount failed"
|
|
pre15_own_guest_mount "$B29_MOUNT" "$label mount"
|
|
B29_MOUNTS="$B29_MOUNTS $B29_MOUNT"
|
|
}
|
|
|
|
drop_guest_ownership()
|
|
{
|
|
kind=$1
|
|
value=$2
|
|
temporary=$PRE15_CASE_TMP/B29-ownership.$$
|
|
awk -v kind="$kind" -v value="$value" \
|
|
'BEGIN { FS = "\\t" } !($1 == kind && $2 == value)' \
|
|
"$PRE15_OWNERSHIP_FILE" >"$temporary"
|
|
mv "$temporary" "$PRE15_OWNERSHIP_FILE"
|
|
}
|
|
|
|
unmount_owned()
|
|
{
|
|
mountpoint=$1
|
|
label=$2
|
|
pre15_guest_ssh_bounded umount "$mountpoint" || \
|
|
pre15_dut_fail "$label unmount failed"
|
|
drop_guest_ownership guest-mount "$mountpoint"
|
|
}
|
|
|
|
unload_owned_kld()
|
|
{
|
|
label=$1
|
|
if ! pre15_guest_ssh_bounded kldunload B29-erofs-zstdio1.ko; then
|
|
pre15_dut_fail "$label module unload did not drain cleanly"
|
|
fi
|
|
drop_guest_ownership guest-kld B29-erofs-zstdio1.ko
|
|
}
|
|
|
|
clear_pool_tunables()
|
|
{
|
|
pre15_guest_ssh_bounded \
|
|
'kenv -u vfs.erofs.stream_pool.global_budget >/dev/null 2>&1 || true; kenv -u vfs.erofs.stream_pool.global_contexts >/dev/null 2>&1 || true; kenv -u vfs.erofs.stream_pool.global_cached_per_codec >/dev/null 2>&1 || true'
|
|
}
|
|
|
|
run_exhaustion_phase()
|
|
{
|
|
codec=$1
|
|
limit_kind=$2
|
|
limit_value=$3
|
|
phase=$4
|
|
clear_pool_tunables
|
|
case "$limit_kind" in
|
|
byte)
|
|
pre15_guest_ssh_bounded kenv \
|
|
"vfs.erofs.stream_pool.global_budget=$limit_value"
|
|
expected_budget=$limit_value
|
|
expected_contexts=64
|
|
;;
|
|
count)
|
|
pre15_guest_ssh_bounded kenv \
|
|
"vfs.erofs.stream_pool.global_contexts=$limit_value"
|
|
expected_budget=134217728
|
|
expected_contexts=$limit_value
|
|
;;
|
|
*) pre15_runner_fail "unknown B29 exhaustion limit: $limit_kind" ;;
|
|
esac
|
|
pre15_guest_ssh_bounded kenv \
|
|
vfs.erofs.stream_pool.global_cached_per_codec=1
|
|
if ! pre15_guest_ssh_bounded kldload /root/B29-erofs-zstdio1.ko \
|
|
>"$artifacts/B29-kldload-$phase.stdout" \
|
|
2>"$artifacts/B29-kldload-$phase.stderr"; then
|
|
pre15_dut_fail "B29 $phase constrained-limit KLD reload failed"
|
|
fi
|
|
pre15_own_guest_kld B29-erofs-zstdio1.ko "$phase exact-source KLD"
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.global_budget; sysctl -n vfs.erofs.stream_pool.global_contexts_limit' \
|
|
>"$artifacts/B29-$phase-effective-limits.txt"
|
|
set -- $(cat "$artifacts/B29-$phase-effective-limits.txt")
|
|
if test "$1" -ne "$expected_budget" || test "$2" -ne "$expected_contexts"; then
|
|
pre15_dut_fail "B29 $phase production tunable did not reach its clamped limit"
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 2))
|
|
|
|
B29_MOUNTS=
|
|
attach_mount "$codec.erofs" "$phase-owner"
|
|
B29_OWNER_MOUNT=$B29_MOUNT
|
|
attach_mount "$codec.erofs" "$phase-b"
|
|
B29_B_MOUNT=$B29_MOUNT
|
|
attach_mount "$codec.erofs" "$phase-c"
|
|
B29_C_MOUNT=$B29_MOUNT
|
|
pre15_guest_ssh_bounded /root/B29-read-probe pass \
|
|
"$B29_OWNER_MOUNT/payload-0.bin" \
|
|
/root/B29-fixtures/source/payload-0.bin || \
|
|
pre15_dut_fail "B29 $phase global exhaustion owner read failed"
|
|
runtime_assertions=$((runtime_assertions + 1))
|
|
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.contexts; sysctl -n vfs.erofs.stream_pool.idle; sysctl -n vfs.erofs.stream_pool.borrowed; sysctl -n vfs.erofs.stream_pool.resident_bytes; sysctl -n vfs.erofs.stream_pool.exhaustions' \
|
|
>"$artifacts/B29-$phase-before-exhaustion.txt"
|
|
set -- $(cat "$artifacts/B29-$phase-before-exhaustion.txt")
|
|
before_contexts=$1
|
|
before_idle=$2
|
|
before_borrowed=$3
|
|
before_resident=$4
|
|
before_exhaustions=$5
|
|
if test "$before_contexts" -ne 1 || test "$before_idle" -ne 1 || \
|
|
test "$before_borrowed" -ne 0 || test "$before_resident" -le 256; then
|
|
pre15_dut_fail "B29 $phase owner did not retain one real idle context"
|
|
fi
|
|
if test "$limit_kind" = byte && \
|
|
test "$before_resident" -ne "$limit_value"; then
|
|
pre15_dut_fail "B29 $phase did not consume the exact production byte budget"
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 4))
|
|
|
|
if ! pre15_guest_ssh_bounded \
|
|
"/root/B29-read-probe errno '$B29_B_MOUNT/payload-1.bin' 12 & p1=\$!; /root/B29-read-probe errno '$B29_C_MOUNT/payload-2.bin' 12 & p2=\$!; wait \$p1; r1=\$?; wait \$p2; r2=\$?; test \$r1 -eq 0 -a \$r2 -eq 0"; then
|
|
pre15_dut_fail "B29 $phase concurrent exhaustion errno was not stable ENOMEM"
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 2))
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.contexts; sysctl -n vfs.erofs.stream_pool.idle; sysctl -n vfs.erofs.stream_pool.borrowed; sysctl -n vfs.erofs.stream_pool.resident_bytes; sysctl -n vfs.erofs.stream_pool.exhaustions' \
|
|
>"$artifacts/B29-$phase-after-exhaustion.txt"
|
|
set -- $(cat "$artifacts/B29-$phase-after-exhaustion.txt")
|
|
if test "$1" -ne 1 || test "$2" -ne 1 || test "$3" -ne 0 || \
|
|
test "$4" -ne "$before_resident" || \
|
|
test "$5" -lt $((before_exhaustions + 2)); then
|
|
pre15_dut_fail "B29 $phase exhaustion changed ownership or missed accounting"
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 5))
|
|
|
|
unmount_owned "$B29_OWNER_MOUNT" "B29 $phase owner"
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.contexts; sysctl -n vfs.erofs.stream_pool.resident_bytes' \
|
|
>"$artifacts/B29-$phase-after-owner-unmount.txt"
|
|
set -- $(cat "$artifacts/B29-$phase-after-owner-unmount.txt")
|
|
if test "$1" -ne 0 || test "$2" -ne 0; then
|
|
pre15_dut_fail "B29 $phase owner unmount did not release global resources"
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 2))
|
|
pre15_guest_ssh_bounded /root/B29-read-probe pass \
|
|
"$B29_B_MOUNT/payload-1.bin" \
|
|
/root/B29-fixtures/source/payload-1.bin || \
|
|
pre15_dut_fail "B29 $phase read did not recover after resource release"
|
|
runtime_assertions=$((runtime_assertions + 1))
|
|
|
|
unmount_owned "$B29_B_MOUNT" "B29 $phase recovery B"
|
|
unmount_owned "$B29_C_MOUNT" "B29 $phase recovery C"
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.contexts; sysctl -n vfs.erofs.stream_pool.resident_bytes; sysctl -n vfs.erofs.stream_pool.borrowed; sysctl -n vfs.erofs.stream_pool.idle' \
|
|
>"$artifacts/B29-$phase-final.txt"
|
|
set -- $(cat "$artifacts/B29-$phase-final.txt")
|
|
if test "$1" -ne 0 || test "$2" -ne 0 || test "$3" -ne 0 || \
|
|
test "$4" -ne 0; then
|
|
pre15_dut_fail "B29 $phase final pool state is not fully drained"
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 4))
|
|
unload_owned_kld "B29 $phase final"
|
|
runtime_assertions=$((runtime_assertions + 1))
|
|
}
|
|
|
|
runtime_assertions=0
|
|
B29_LZMA_CONTEXT_BYTES=
|
|
B29_ZSTD_CONTEXT_BYTES=
|
|
B29_MOUNTS=
|
|
for codec in lzma deflate zstd; do
|
|
attach_mount "$codec.erofs" "$codec"
|
|
for index in 0 1 2; do
|
|
pre15_guest_ssh_bounded /root/B29-read-probe pass \
|
|
"$B29_MOUNT/payload-$index.bin" \
|
|
"/root/B29-fixtures/source/payload-$index.bin" || \
|
|
pre15_dut_fail "$codec ordinary payload-$index read failed"
|
|
runtime_assertions=$((runtime_assertions + 1))
|
|
done
|
|
case "$codec" in
|
|
lzma|zstd)
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.contexts; sysctl -n vfs.erofs.stream_pool.idle; sysctl -n vfs.erofs.stream_pool.resident_bytes' \
|
|
>"$artifacts/B29-$codec-context-bytes.txt"
|
|
set -- $(cat "$artifacts/B29-$codec-context-bytes.txt")
|
|
if test "$1" -ne 1 || test "$2" -ne 1 || test "$3" -le 256; then
|
|
pre15_dut_fail "B29 $codec did not retain one charged production context"
|
|
fi
|
|
case "$codec" in
|
|
lzma) B29_LZMA_CONTEXT_BYTES=$3 ;;
|
|
zstd) B29_ZSTD_CONTEXT_BYTES=$3 ;;
|
|
esac
|
|
runtime_assertions=$((runtime_assertions + 3))
|
|
;;
|
|
esac
|
|
unmount_owned "$B29_MOUNT" "B29 $codec ordinary"
|
|
done
|
|
for codec in lzma-max zstd-max; do
|
|
attach_mount "$codec.erofs" "$codec"
|
|
pre15_guest_ssh_bounded /root/B29-read-probe pass \
|
|
"$B29_MOUNT/payload-0.bin" \
|
|
/root/B29-fixtures/source/payload-0.bin || \
|
|
pre15_dut_fail "$codec maximum-parameter workload failed"
|
|
runtime_assertions=$((runtime_assertions + 1))
|
|
unmount_owned "$B29_MOUNT" "B29 $codec maximum-parameter"
|
|
done
|
|
|
|
for codec in lzma zstd; do
|
|
B29_BAD_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
|
-f "/root/B29-fixtures/images/$codec-max-over.erofs") || \
|
|
pre15_dut_fail "$codec over-limit md attach failed"
|
|
pre15_own_guest_md "$B29_BAD_MD" "$codec over-limit md"
|
|
B29_BAD_MOUNT=/mnt/pre15-b29-$codec-over
|
|
pre15_guest_ssh_bounded mkdir -p "$B29_BAD_MOUNT"
|
|
pre15_guest_ssh_bounded /root/B29-mount-probe "/dev/$B29_BAD_MD" \
|
|
"$B29_BAD_MOUNT" 45 || \
|
|
pre15_dut_fail "$codec over-limit config did not return EOPNOTSUPP"
|
|
runtime_assertions=$((runtime_assertions + 1))
|
|
done
|
|
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.creations; sysctl -n vfs.erofs.stream_pool.reuses' \
|
|
>"$artifacts/B29-pool-legal-metrics.txt"
|
|
set -- $(cat "$artifacts/B29-pool-legal-metrics.txt")
|
|
if test "$1" -lt 5 || test "$2" -lt 6; then
|
|
pre15_dut_fail 'B29 legal workloads did not create and reuse real contexts'
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 2))
|
|
pre15_guest_ssh_bounded \
|
|
'sysctl -n vfs.erofs.stream_pool.contexts; sysctl -n vfs.erofs.stream_pool.resident_bytes' \
|
|
>"$artifacts/B29-pool-after-legal-unmount.txt"
|
|
set -- $(cat "$artifacts/B29-pool-after-legal-unmount.txt")
|
|
if test "$1" -ne 0 || test "$2" -ne 0; then
|
|
pre15_dut_fail 'B29 legal workload unmount did not drain all contexts'
|
|
fi
|
|
runtime_assertions=$((runtime_assertions + 2))
|
|
unload_owned_kld 'B29 first'
|
|
runtime_assertions=$((runtime_assertions + 1))
|
|
test -n "$B29_LZMA_CONTEXT_BYTES" || \
|
|
pre15_runner_fail 'B29 LZMA production context size was not recorded'
|
|
test -n "$B29_ZSTD_CONTEXT_BYTES" || \
|
|
pre15_runner_fail 'B29 Zstd production context size was not recorded'
|
|
run_exhaustion_phase lzma byte "$B29_LZMA_CONTEXT_BYTES" lzma-byte
|
|
run_exhaustion_phase zstd byte "$B29_ZSTD_CONTEXT_BYTES" zstd-byte
|
|
run_exhaustion_phase lzma count 1 global-count
|
|
clear_pool_tunables
|
|
|
|
if test "$runtime_assertions" -ne 90; then
|
|
pre15_runner_fail "B29 QEMU assertion count mismatch: $runtime_assertions"
|
|
fi
|
|
pre15_guest_ssh_bounded dmesg >"$artifacts/B29-dmesg.txt"
|
|
pre15_target_reached
|
|
printf '%s\n' \
|
|
'B29 FreeBSD 15 QEMU context-pool PASS' \
|
|
'ordinary=9 charged-codec-state=6 maximum-parameter=2 over-limit=2 lifecycle=5 constrained-phases=66 total=90' \
|
|
'LZMA-byte, Zstd-byte, and global-count cross-mount exhaustion returned ENOMEM=12; each owner drain reached zero and each retry succeeded; full feature suite NOT_RUN'
|