Files
erofs-freebsd-out-tree/tests/pre15/cases/B29-context-pool.sh
T
2026-08-18 09:20:44 +02:00

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'