642 lines
26 KiB
Bash
Executable File
642 lines
26 KiB
Bash
Executable File
#!/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"
|
|
|
|
baseline=93d0c64e321f50cf68026c3f1c0c95293bf17302
|
|
case_script=$PRE15_DUT/tests/pre15/cases/B33-cache-policy.sh
|
|
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
|
|
gate_dir=$PRE15_DUT/tests/pre15/gates
|
|
oracle=$fixture_dir/B33-cache-oracle.c
|
|
qemu_spec=$fixture_dir/B33-cache-state.json
|
|
qemu_generator=$fixture_dir/B33-cache-generate.py
|
|
qemu_runner=$fixture_dir/B33-qemu-run.sh
|
|
b32_probe=$fixture_dir/B32-cache-probe.c
|
|
kld_builder=$fixture_dir/B28-build-kld.sh
|
|
gate_input=$gate_dir/P15-038-input.json
|
|
gate_result=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/result.json
|
|
gate_benefit=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/benefit.json
|
|
gate_state=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/state-model.json
|
|
gate_hashes=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/SHA256SUMS
|
|
artifacts=$PRE15_RUN_DIR/artifacts
|
|
|
|
for tool in cc git python3 sha256sum timeout; do
|
|
command -v "$tool" >/dev/null 2>&1 || \
|
|
pre15_infra_blocked "missing B33 host tool: $tool"
|
|
done
|
|
for fixture in "$case_script" "$oracle" "$qemu_spec" "$qemu_generator" "$qemu_runner" \
|
|
"$b32_probe" "$kld_builder" "$gate_input" "$gate_result" \
|
|
"$gate_benefit" "$gate_state" "$gate_hashes"; do
|
|
pre15_record_fixture "b33-$(basename "$fixture")" "$fixture"
|
|
done
|
|
for source in internal.h zdata.c; do
|
|
pre15_record_fixture "b33-$source" "$PRE15_DUT/src/$source"
|
|
done
|
|
mkdir -p "$artifacts"
|
|
|
|
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
|
|
pre15_target_reached
|
|
else
|
|
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$gate_input" \
|
|
"$gate_result" "$gate_benefit" "$gate_state" \
|
|
"$artifacts/B33-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])
|
|
baseline = sys.argv[3]
|
|
implementation = "bc56f830918b76029871b60cca2e53992de70a2e"
|
|
current_owners = {
|
|
"internal.h": "aa20623132b6821c6ad4651b263fc0ac0f7a14c8",
|
|
"zdata.c": implementation,
|
|
}
|
|
gate_input = json.loads(Path(sys.argv[4]).read_text(encoding="ascii"))
|
|
gate_result = json.loads(Path(sys.argv[5]).read_text(encoding="ascii"))
|
|
gate_benefit = json.loads(Path(sys.argv[6]).read_text(encoding="ascii"))
|
|
gate_state = json.loads(Path(sys.argv[7]).read_text(encoding="ascii"))
|
|
report_path = Path(sys.argv[8])
|
|
|
|
|
|
def fail(message: str) -> None:
|
|
raise SystemExit(message)
|
|
|
|
|
|
def committed(commit: str, relative: str) -> str:
|
|
completed = subprocess.run(
|
|
["git", "-C", str(root), "show", f"{commit}:{relative}"],
|
|
check=False,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if completed.returncode != 0:
|
|
fail(f"cannot read B33 source {commit}:{relative}: {completed.stderr.strip()}")
|
|
return completed.stdout
|
|
|
|
|
|
def function(source: str, name: str) -> str:
|
|
match = re.search(rf"\n(?:static\s+)?[^\n]+\n{name}\([^{{]+\n\{{", source)
|
|
if match is None:
|
|
fail(f"cannot locate function {name}")
|
|
start = match.start() + 1
|
|
brace = source.find("{", match.start())
|
|
depth = 0
|
|
for index in range(brace, len(source)):
|
|
if source[index] == "{":
|
|
depth += 1
|
|
elif source[index] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return source[start : index + 1]
|
|
fail(f"unterminated function {name}")
|
|
|
|
|
|
expected_changed = {
|
|
"repo-pre-15/src/internal.h",
|
|
"repo-pre-15/src/zdata.c",
|
|
"repo-pre-15/tests/pre15/cases/B33-cache-policy.sh",
|
|
"repo-pre-15/tests/pre15/fixtures/B33-cache-generate.py",
|
|
"repo-pre-15/tests/pre15/fixtures/B33-cache-oracle.c",
|
|
"repo-pre-15/tests/pre15/fixtures/B33-cache-state.json",
|
|
"repo-pre-15/tests/pre15/fixtures/B33-qemu-run.sh",
|
|
}
|
|
changed = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
str(root),
|
|
"diff-tree",
|
|
"--no-commit-id",
|
|
"--name-only",
|
|
"-r",
|
|
implementation,
|
|
],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
timeout=30,
|
|
).stdout.splitlines()
|
|
if set(changed) != expected_changed or len(changed) != len(expected_changed):
|
|
fail(f"B33 implementation write set mismatch: {changed!r}")
|
|
|
|
for name, commit in (("baseline", baseline), ("implementation", implementation)):
|
|
if subprocess.check_output(
|
|
["git", "-C", str(root), "rev-parse", commit], text=True
|
|
).strip() != commit:
|
|
fail(f"B33 {name} identity changed")
|
|
|
|
internal = (dut / "src/internal.h").read_text(encoding="utf-8")
|
|
zdata = (dut / "src/zdata.c").read_text(encoding="utf-8")
|
|
implemented_internal = committed(implementation, "repo-pre-15/src/internal.h")
|
|
implemented_zdata = committed(implementation, "repo-pre-15/src/zdata.c")
|
|
for name, source in (("internal.h", internal), ("zdata.c", zdata)):
|
|
owner = current_owners[name]
|
|
if source != committed(owner, f"repo-pre-15/src/{name}"):
|
|
fail(f"current B33 source differs from accepted owner {owner}: {name}")
|
|
|
|
baseline_zdata = committed(baseline, "repo-pre-15/src/zdata.c")
|
|
for preserved in ("z_erofs_decode_length", "z_erofs_decode_extent"):
|
|
if function(implemented_zdata, preserved) != function(baseline_zdata, preserved):
|
|
fail(f"B25/B27/B28 read/decode path changed in B33: {preserved}")
|
|
|
|
do_read = function(implemented_zdata, "z_erofs_do_read")
|
|
baseline_do_read = function(baseline_zdata, "z_erofs_do_read")
|
|
bypass_delta = (
|
|
"\t\t\t} else\n"
|
|
"\t\t\t\tz_erofs_extent_cache_record_bypass(sbi, &map);"
|
|
)
|
|
if do_read.count(bypass_delta) != 1 or do_read.replace(bypass_delta, "\t\t\t}") != baseline_do_read:
|
|
fail("B33 z_erofs_do_read differs beyond policy-rejection accounting")
|
|
|
|
required_b32 = (
|
|
"cache->nid == vi->nid",
|
|
"cache->decoded_size == decoded_size",
|
|
"cache->map.m_pa == map->m_pa",
|
|
"cache->map.m_la == map->m_la",
|
|
"cache->map.m_plen == map->m_plen",
|
|
"cache->map.m_llen == map->m_llen",
|
|
"cache->map.m_deviceid == map->m_deviceid",
|
|
"cache->map.m_algorithmformat == map->m_algorithmformat",
|
|
"cache->map.m_flags == map->m_flags",
|
|
"cache->state == EROFS_ZCACHE_INFLIGHT || cache->waiters != 0",
|
|
"cv_wait(&cache->cv, &sbi->z_extent_cache_lock)",
|
|
"cache->error = error",
|
|
"cache->closing = true",
|
|
"Z_EROFS_CACHE_BYPASS",
|
|
"free(decoded, M_EROFS);",
|
|
)
|
|
missing = [token for token in required_b32 if token not in zdata]
|
|
if missing:
|
|
fail(f"B32 key/inflight/failure/fallback token missing: {missing!r}")
|
|
|
|
policy = function(zdata, "z_erofs_extent_cache_eligible")
|
|
for named_codec in (
|
|
"Z_EROFS_COMPRESSION_LZ4",
|
|
"Z_EROFS_COMPRESSION_LZMA",
|
|
"Z_EROFS_COMPRESSION_DEFLATE",
|
|
"Z_EROFS_COMPRESSION_ZSTD",
|
|
):
|
|
if named_codec in policy:
|
|
fail(f"B33 admission still names a codec: {named_codec}")
|
|
for token in (
|
|
"map->m_algorithmformat < Z_EROFS_COMPRESSION_MAX",
|
|
"len <= cache->budget_bytes",
|
|
"decode_work >= cache->minimum_decode_work",
|
|
"z_erofs_cache_enabled != 0",
|
|
):
|
|
if token not in policy:
|
|
fail(f"codec-neutral work/size policy token is absent: {token}")
|
|
if "map->m_algorithmformat == Z_EROFS_COMPRESSION_LZMA" in zdata:
|
|
fail("LZMA-only admission branch remains")
|
|
record_bypass = function(zdata, "z_erofs_extent_cache_record_bypass")
|
|
if "++cache->metrics[map->m_algorithmformat].bypasses" not in record_bypass or (
|
|
"z_erofs_extent_cache_record_bypass(sbi, &map)" not in do_read
|
|
):
|
|
fail("B33 policy rejection is absent from four-codec accounting")
|
|
|
|
budget = gate_input["budget"]
|
|
expected_macros = {
|
|
"EROFS_ZCACHE_MOUNT_HARD_BUDGET": budget["per_mount_bytes"] // 1024,
|
|
"EROFS_ZCACHE_GLOBAL_HARD_BUDGET": budget["global_bytes"] // 1024,
|
|
"EROFS_ZCACHE_MIN_DECODE_WORK": budget["minimum_decode_work_bytes"] // 1024,
|
|
}
|
|
for name, kib in expected_macros.items():
|
|
if f"#define {name} ({kib}UL * 1024)" not in zdata:
|
|
fail(f"B33 hard policy differs from G05: {name}")
|
|
for token in (
|
|
'TUNABLE_INT("vfs.erofs.decoded_cache.enabled"',
|
|
'TUNABLE_ULONG("vfs.erofs.decoded_cache.mount_budget"',
|
|
'TUNABLE_ULONG("vfs.erofs.decoded_cache.global_budget"',
|
|
'TUNABLE_ULONG("vfs.erofs.decoded_cache.minimum_decode_work"',
|
|
"MIN(z_erofs_cache_mount_budget,",
|
|
"MIN(z_erofs_cache_global_budget,",
|
|
"EVENTHANDLER_REGISTER(vm_lowmem",
|
|
"EVENTHANDLER_DEREGISTER(vm_lowmem",
|
|
"LIST_FOREACH(sbi, &z_erofs_cache_mounts, z_extent_cache_link)",
|
|
"mtx_trylock(&sbi->z_extent_cache_lock)",
|
|
):
|
|
if token not in zdata:
|
|
fail(f"B33 budget/config/reclaim token is absent: {token}")
|
|
|
|
claim = function(zdata, "z_erofs_extent_cache_claim")
|
|
if claim.find("z_erofs_extent_cache_reserve(decoded_size)") > claim.find(
|
|
"cache->state = EROFS_ZCACHE_INFLIGHT"
|
|
):
|
|
fail("B33 does not reserve before inflight decode")
|
|
drop_position = claim.find("z_erofs_extent_cache_drop_locked(cache, true, false)")
|
|
empty_position = claim.find("cache->state = EROFS_ZCACHE_EMPTY", drop_position)
|
|
reserve_position = claim.find("z_erofs_extent_cache_reserve(decoded_size)")
|
|
if drop_position < 0 or not (drop_position < empty_position < reserve_position):
|
|
fail("B33 replacement reservation failure can leave READY without data")
|
|
complete = function(zdata, "z_erofs_extent_cache_complete")
|
|
if complete.find("z_erofs_extent_cache_release(cache->charged_bytes)") > complete.find(
|
|
"cache->state = EROFS_ZCACHE_FAILED"
|
|
):
|
|
fail("B33 failed decode publishes before releasing reservation")
|
|
|
|
for field in (
|
|
"hits",
|
|
"misses",
|
|
"bypasses",
|
|
"evictions",
|
|
"reclaims",
|
|
"resident_bytes",
|
|
"charged_bytes",
|
|
"budget_bytes",
|
|
"minimum_decode_work",
|
|
"metrics[Z_EROFS_COMPRESSION_MAX]",
|
|
):
|
|
if field not in internal:
|
|
fail(f"B33 four-codec accounting field is absent: {field}")
|
|
for forbidden in ("shrinker", "workqueue", "work_struct", "wait_on_bit", "folio"):
|
|
if forbidden in internal + zdata:
|
|
fail(f"Linux-only cache primitive introduced: {forbidden}")
|
|
|
|
if gate_result.get("status") != "GO" or gate_result.get("b33") != "AUTHORIZED":
|
|
fail("P15-038 G05 result is not GO")
|
|
if gate_state.get("status") != "PASS" or gate_state["budget"]["global_remaining"] != 0:
|
|
fail("P15-038 state model is not closed")
|
|
if gate_benefit.get("codecs_meeting_threshold", 0) < 2 or any(
|
|
len(summary["current_ns"]) != 5 or len(summary["candidate_ns"]) != 5
|
|
for summary in gate_benefit.get("summaries", [])
|
|
):
|
|
fail("P15-038 fixed five-sample benefit is incomplete")
|
|
|
|
report = {
|
|
"baseline": baseline,
|
|
"batch": "B33",
|
|
"b32_contract_preserved": True,
|
|
"codec_neutral": True,
|
|
"current_owners": current_owners,
|
|
"gate_codecs_meeting_threshold": gate_benefit["codecs_meeting_threshold"],
|
|
"global_hard_budget": budget["global_bytes"],
|
|
"implementation": implementation,
|
|
"lock_order": ["global-list", "mount-cache", "global-budget"],
|
|
"mount_hard_budget": budget["per_mount_bytes"],
|
|
"no_cache_fallback": True,
|
|
"reclaim": "FreeBSD-vm_lowmem",
|
|
"write_set": sorted(changed),
|
|
}
|
|
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_runner_fail 'B33 source, gate, write-set, or preservation audit failed'
|
|
fi
|
|
|
|
if ! (cd "$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038" && \
|
|
timeout -k 5 30 sha256sum -c SHA256SUMS) \
|
|
>"$artifacts/gate-manifest.stdout" 2>"$artifacts/gate-manifest.stderr"; then
|
|
pre15_runner_fail 'P15-038 gate evidence manifest failed'
|
|
fi
|
|
if ! cc -std=c11 -O2 -Wall -Wextra -Werror "$oracle" \
|
|
-o "$PRE15_CASE_TMP/B33-cache-oracle" \
|
|
>"$artifacts/oracle-build.stdout" 2>"$artifacts/oracle-build.stderr"; then
|
|
pre15_runner_fail 'B33 cache budget oracle did not compile'
|
|
fi
|
|
if ! timeout -k 5 60 "$PRE15_CASE_TMP/B33-cache-oracle" \
|
|
>"$artifacts/oracle.json" 2>"$artifacts/oracle.stderr"; then
|
|
pre15_dut_fail 'B33 cache budget/accounting oracle failed'
|
|
fi
|
|
if ! python3 -B - "$artifacts/oracle.json" <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
result = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
|
if result.get("status") != "PASS":
|
|
raise SystemExit("B33 oracle status is not PASS")
|
|
if result.get("global_peak") != result.get("global_limit") or result.get("global_remaining") != 0:
|
|
raise SystemExit("B33 oracle global budget did not close")
|
|
metrics = result.get("metrics", [])
|
|
if len(metrics) != 4 or [item.get("codec") for item in metrics] != list(range(4)):
|
|
raise SystemExit("B33 four-codec metric set is incomplete")
|
|
for item in metrics:
|
|
if item.get("hits", 0) < 1 or item.get("misses", 0) < 1 or item.get("evictions", 0) < 1:
|
|
raise SystemExit(f"B33 codec metric is incomplete: {item!r}")
|
|
if item.get("resident_bytes") != 0:
|
|
raise SystemExit(f"B33 codec resident bytes leaked: {item!r}")
|
|
PY
|
|
then
|
|
pre15_dut_fail 'B33 four-codec accounting result is incomplete'
|
|
fi
|
|
|
|
sha256sum "$artifacts/B33-source-audit.json" "$artifacts/oracle.json" \
|
|
"$artifacts/gate-manifest.stdout" >"$artifacts/SHA256SUMS"
|
|
fi
|
|
pre15_target_reached
|
|
|
|
if test "${PRE15_MODE:-host}" != qemu; then
|
|
printf '%s\n' \
|
|
'B33-cache-budget host PASS' \
|
|
'Four-codec admission/accounting, mount/global hard budgets, exhaustion fallback, reclaim, failure retry, disable, and eviction hash PASS' \
|
|
'P15-038 host codec-cost gate replay manifest PASS; no guest vnode performance claimed' \
|
|
'TC084/TC105/TC129 NOT_RUN (not necessary)' \
|
|
'QEMU TC168, TC130, and full feature suite NOT_RUN in host mode'
|
|
exit 0
|
|
fi
|
|
|
|
test "${PRE15_B32_SCENARIO:-}" = TC168-cache-inflight || \
|
|
pre15_runner_fail 'B33 QEMU mode requires TC168-cache-inflight'
|
|
for tool in clang cmp diff file mkfs.erofs nm scp tar timeout; do
|
|
command -v "$tool" >/dev/null 2>&1 || \
|
|
pre15_infra_blocked "missing B33 QEMU tool: $tool"
|
|
done
|
|
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
|
|
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
|
|
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
|
|
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
|
|
|
|
first=$PRE15_CASE_TMP/fixtures-first
|
|
second=$PRE15_CASE_TMP/fixtures-second
|
|
if ! (umask 022 && timeout -k 5 180 python3 -B "$qemu_generator" \
|
|
--spec "$qemu_spec" --output "$first") \
|
|
>"$artifacts/generate-first.json" 2>"$artifacts/generate-first.stderr" || \
|
|
! (umask 022 && timeout -k 5 180 python3 -B "$qemu_generator" \
|
|
--spec "$qemu_spec" --output "$second") \
|
|
>"$artifacts/generate-second.json" 2>"$artifacts/generate-second.stderr"; then
|
|
pre15_runner_fail 'B33 fixture generation failed'
|
|
fi
|
|
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
|
|
pre15_runner_fail 'B33 fixtures are not byte reproducible'
|
|
fi
|
|
cp "$first/fixture-index.json" "$artifacts/B33-fixture-index.json"
|
|
|
|
module=$PRE15_CASE_TMP/B33-erofs-zstdio0.ko
|
|
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
|
|
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
|
|
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
|
|
pre15_dut_fail 'B33 cross-target zstdio0 KLD build failed'
|
|
fi
|
|
pre15_record_module "$module"
|
|
file "$module" >"$artifacts/kld-file.txt"
|
|
sha256sum "$module" >"$artifacts/kld-sha256.txt"
|
|
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
|
|
|
|
fixture_archive=$PRE15_CASE_TMP/B33-fixtures.tar.gz
|
|
tar -C "$first" -czf "$fixture_archive" .
|
|
|
|
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" \
|
|
-i "$PRE15_QEMU_SSH_KEY" -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/B33-fixtures.tar.gz || \
|
|
! pre15_scp "$b32_probe" /root/B33-cache-probe.c || \
|
|
! pre15_scp "$module" /root/B33-erofs-zstdio0.ko; then
|
|
pre15_infra_blocked 'could not transfer B33 module, probe, or fixtures'
|
|
fi
|
|
if ! pre15_guest_ssh_bounded \
|
|
'rm -rf /root/B33-fixtures && mkdir /root/B33-fixtures && tar -xzf /root/B33-fixtures.tar.gz -C /root/B33-fixtures && cc -O2 -Wall -Wextra -Werror -pthread -o /root/B33-cache-probe /root/B33-cache-probe.c'; then
|
|
pre15_infra_blocked 'could not prepare B33 guest fixtures/probe'
|
|
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
|
|
module_sha256=$(sha256sum "$module" | awk '{print $1}')
|
|
guest_module_sha256=$(pre15_guest_ssh_bounded sha256 -q /root/B33-erofs-zstdio0.ko) || \
|
|
pre15_infra_blocked 'could not hash transferred B33 KLD'
|
|
printf 'host=%s guest=%s\n' "$module_sha256" "$guest_module_sha256" \
|
|
>"$artifacts/guest-module-sha256.txt"
|
|
test "$guest_module_sha256" = "$module_sha256" || \
|
|
pre15_runner_fail 'transferred B33 KLD hash differs from host module'
|
|
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-load.txt"
|
|
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before-load.txt"
|
|
cache_tunable=vfs.erofs.decoded_cache.minimum_decode_work
|
|
cache_tunable_before=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
|
|
printf 'name=%s before=%s test-value=0\n' "$cache_tunable" \
|
|
"${cache_tunable_before:-unset}" >"$artifacts/cache-tunable.txt"
|
|
if ! pre15_guest_ssh_bounded kenv "$cache_tunable=0" \
|
|
>"$artifacts/cache-tunable-set.stdout" \
|
|
2>"$artifacts/cache-tunable-set.stderr"; then
|
|
pre15_infra_blocked 'could not force B33 decoded-cache admission'
|
|
fi
|
|
if ! pre15_guest_ssh_bounded kldload /root/B33-erofs-zstdio0.ko \
|
|
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
|
|
if test -n "$cache_tunable_before"; then
|
|
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" || true
|
|
else
|
|
pre15_guest_ssh_bounded kenv -u "$cache_tunable" || true
|
|
fi
|
|
if grep -q 'module already loaded or in kernel' \
|
|
"$artifacts/kldload.stderr"; then
|
|
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
|
|
fi
|
|
pre15_dut_fail 'B33 exact-source zstdio0 KLD failed to load'
|
|
fi
|
|
if test -n "$cache_tunable_before"; then
|
|
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" \
|
|
>"$artifacts/cache-tunable-restore.stdout" \
|
|
2>"$artifacts/cache-tunable-restore.stderr" || \
|
|
pre15_infra_blocked 'could not restore B33 cache tunable'
|
|
else
|
|
pre15_guest_ssh_bounded kenv -u "$cache_tunable" \
|
|
>"$artifacts/cache-tunable-restore.stdout" \
|
|
2>"$artifacts/cache-tunable-restore.stderr" || \
|
|
pre15_infra_blocked 'could not remove B33 cache tunable override'
|
|
fi
|
|
cache_tunable_after=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
|
|
printf 'after=%s\n' "${cache_tunable_after:-unset}" \
|
|
>>"$artifacts/cache-tunable.txt"
|
|
if test "$cache_tunable_after" != "$cache_tunable_before"; then
|
|
pre15_runner_fail 'B33 cache tunable was not restored exactly'
|
|
fi
|
|
if ! pre15_guest_ssh_bounded kldstat -q -m erofs; then
|
|
pre15_dut_fail 'B33 kldload returned success without erofs.1 ownership'
|
|
fi
|
|
if ! pre15_guest_ssh_bounded kldstat -q -n B33-erofs-zstdio0.ko; then
|
|
pre15_dut_fail 'B33 kldload returned success without exact KLD file ownership'
|
|
fi
|
|
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-load.txt"
|
|
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before.txt"
|
|
diff -u "$artifacts/guest-dmesg-before-load.txt" \
|
|
"$artifacts/guest-dmesg-before.txt" >"$artifacts/guest-dmesg-load.diff" || true
|
|
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg-load.diff" \
|
|
>"$artifacts/guest-dmesg-load-added.txt"
|
|
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
|
|
"$artifacts/guest-dmesg-load-added.txt"; then
|
|
pre15_dut_fail 'B33 exact-source KLD load produced kernel or linker errors'
|
|
fi
|
|
pre15_own_guest_kld B33-erofs-zstdio0.ko 'B33 exact-source KLD'
|
|
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-trace.txt"
|
|
if ! pre15_guest_ssh_bounded command -v dtrace \
|
|
>"$artifacts/guest-dtrace-command.txt" 2>"$artifacts/guest-dtrace-command.stderr"; then
|
|
pre15_infra_blocked 'guest dtrace tool is unavailable for B33 call proof'
|
|
fi
|
|
set +e
|
|
pre15_guest_ssh_bounded kldload dtraceall \
|
|
>"$artifacts/guest-dtrace-kldload.stdout" \
|
|
2>"$artifacts/guest-dtrace-kldload.stderr"
|
|
dtrace_load_rc=$?
|
|
set -e
|
|
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-trace.txt"
|
|
awk 'NR == FNR { if (FNR > 1) seen[$5] = 1; next }
|
|
FNR > 1 && !seen[$5] { print $1 "\t" $5 }' \
|
|
"$artifacts/guest-kldstat-before-trace.txt" \
|
|
"$artifacts/guest-kldstat-after-trace.txt" \
|
|
>"$artifacts/guest-trace-klds-all.tsv"
|
|
awk '$2 == "dtraceall.ko"' "$artifacts/guest-trace-klds-all.tsv" \
|
|
>"$artifacts/guest-trace-klds.tsv"
|
|
while IFS="$(printf '\t')" read -r trace_kld_id trace_kld_name; do
|
|
test -n "$trace_kld_id" || continue
|
|
pre15_own_guest_kld "$trace_kld_name" 'B33 FBT call-proof dependency'
|
|
done <"$artifacts/guest-trace-klds.tsv"
|
|
test "$dtrace_load_rc" -eq 0 || \
|
|
pre15_infra_blocked 'guest dtraceall KLD failed to load for B33 call proof'
|
|
if ! pre15_guest_ssh_bounded \
|
|
"dtrace -l -n 'fbt::z_erofs_extent_cache_init:entry' -n 'fbt::z_erofs_do_read:entry'" \
|
|
>"$artifacts/guest-dtrace-probe-list.txt" \
|
|
2>"$artifacts/guest-dtrace-probe-list.stderr"; then
|
|
pre15_infra_blocked 'B33 cache init/read FBT probes are unavailable'
|
|
fi
|
|
grep -q 'z_erofs_extent_cache_init.*entry' \
|
|
"$artifacts/guest-dtrace-probe-list.txt" || \
|
|
pre15_runner_fail 'B33 cache init FBT probe was not listed exactly'
|
|
grep -q 'z_erofs_do_read.*entry' \
|
|
"$artifacts/guest-dtrace-probe-list.txt" || \
|
|
pre15_runner_fail 'B33 compressed-read FBT probe was not listed exactly'
|
|
|
|
b33_unown()
|
|
{
|
|
kind=$1
|
|
value=$2
|
|
tmp=$PRE15_CASE_TMP/ownership.$$
|
|
awk -F ' ' -v kind="$kind" -v value="$value" \
|
|
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
|
|
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
|
|
}
|
|
|
|
b33_attach()
|
|
{
|
|
image=$1
|
|
label=$2
|
|
B33_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
|
-f "/root/B33-fixtures/images/$image") || \
|
|
pre15_dut_fail "$label md attach failed"
|
|
case "$B33_MD" in
|
|
md[0-9]*) ;;
|
|
*) pre15_runner_fail "unexpected B33 md unit: $B33_MD" ;;
|
|
esac
|
|
pre15_own_guest_md "$B33_MD" "$label md"
|
|
B33_MOUNT=/mnt/pre15-b33-$label
|
|
pre15_guest_ssh_bounded mkdir -p "$B33_MOUNT"
|
|
if test "${B33_TRACE_MOUNT:-0}" = 1; then
|
|
mount_trace=/tmp/pre15-b33-cache-init.trace
|
|
pre15_guest_ssh_bounded \
|
|
"dtrace -q -o '$mount_trace' -n 'fbt::z_erofs_extent_cache_init:entry { @calls = count(); } END { printa(@calls); }' -c 'mount -t erofs -o ro /dev/$B33_MD $B33_MOUNT'" \
|
|
>"$artifacts/valid-mount.stdout" \
|
|
2>"$artifacts/valid-mount.stderr" || \
|
|
pre15_dut_fail "$label traced mount failed"
|
|
pre15_guest_ssh_bounded cat "$mount_trace" \
|
|
>"$artifacts/guest-cache-init-fbt.txt"
|
|
if ! awk '$1 ~ /^[0-9]+$/ && $1 > 0 { found = 1 }
|
|
END { exit !found }' "$artifacts/guest-cache-init-fbt.txt"; then
|
|
pre15_dut_fail 'B33 guest mount did not call decoded-cache init'
|
|
fi
|
|
else
|
|
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B33_MD" "$B33_MOUNT" || \
|
|
pre15_dut_fail "$label mount failed"
|
|
fi
|
|
pre15_own_guest_mount "$B33_MOUNT" "$label mount"
|
|
}
|
|
|
|
b33_detach()
|
|
{
|
|
if pre15_guest_ssh_bounded mount | grep -F " on $B33_MOUNT " >/dev/null; then
|
|
pre15_guest_ssh_bounded umount "$B33_MOUNT" || \
|
|
pre15_dut_fail "$B33_MOUNT unmount failed"
|
|
fi
|
|
b33_unown guest-mount "$B33_MOUNT"
|
|
pre15_guest_ssh_bounded mdconfig -d -u "$B33_MD" || \
|
|
pre15_dut_fail "$B33_MD detach failed"
|
|
b33_unown guest-md "$B33_MD"
|
|
}
|
|
|
|
reference=/root/B33-fixtures/source/payload.bin
|
|
B33_TRACE_MOUNT=1
|
|
b33_attach lz4-valid.erofs valid
|
|
unset B33_TRACE_MOUNT
|
|
trace_file=/tmp/pre15-b33-do-read.trace
|
|
if ! pre15_guest_ssh_bounded \
|
|
"dtrace -q -o '$trace_file' -n 'fbt::z_erofs_do_read:entry { @calls = count(); } END { printa(@calls); }' -c '/root/B33-cache-probe concurrent $B33_MOUNT/payload.bin $reference 0 64 4 0 65536'" \
|
|
>"$artifacts/valid-concurrent.stdout" \
|
|
2>"$artifacts/valid-concurrent.stderr"; then
|
|
pre15_dut_fail 'TC168 valid full-extent concurrent reads diverged'
|
|
fi
|
|
if ! grep -qx 'concurrent PASS workers=64 loops=4 assertions=256 errno=0 offset=0 length=65536' \
|
|
"$artifacts/valid-concurrent.stdout"; then
|
|
pre15_runner_fail 'TC168 valid assertion count was not exactly 256'
|
|
fi
|
|
pre15_guest_ssh_bounded cat "$trace_file" \
|
|
>"$artifacts/guest-do-read-fbt.txt"
|
|
if ! awk '$1 ~ /^[0-9]+$/ && $1 > 0 { found = 1 }
|
|
END { exit !found }' "$artifacts/guest-do-read-fbt.txt"; then
|
|
pre15_dut_fail 'B33 guest reads did not call the target read implementation'
|
|
fi
|
|
b33_detach
|
|
|
|
b33_attach lz4-truncated.erofs truncated
|
|
if ! pre15_guest_ssh_bounded /root/B33-cache-probe concurrent \
|
|
"$B33_MOUNT/payload.bin" "$reference" 97 1 1 0 65536 \
|
|
>"$artifacts/failure-single.stdout" \
|
|
2>"$artifacts/failure-single.stderr"; then
|
|
pre15_dut_fail 'TC168 single corrupt read did not return EINTEGRITY'
|
|
fi
|
|
if ! grep -qx 'concurrent PASS workers=1 loops=1 assertions=1 errno=97 offset=0 length=65536' \
|
|
"$artifacts/failure-single.stdout"; then
|
|
pre15_runner_fail 'TC168 single corrupt assertion count was not 1'
|
|
fi
|
|
if ! pre15_guest_ssh_bounded /root/B33-cache-probe concurrent \
|
|
"$B33_MOUNT/payload.bin" "$reference" 97 64 2 0 65536 \
|
|
>"$artifacts/failure-concurrent.stdout" \
|
|
2>"$artifacts/failure-concurrent.stderr"; then
|
|
pre15_dut_fail 'TC168 failure waiters did not receive EINTEGRITY'
|
|
fi
|
|
if ! grep -qx 'concurrent PASS workers=64 loops=2 assertions=128 errno=97 offset=0 length=65536' \
|
|
"$artifacts/failure-concurrent.stdout"; then
|
|
pre15_runner_fail 'TC168 failure assertion count was not exactly 128'
|
|
fi
|
|
b33_detach
|
|
|
|
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-after.txt"
|
|
diff -u "$artifacts/guest-dmesg-before.txt" \
|
|
"$artifacts/guest-dmesg-after.txt" >"$artifacts/guest-dmesg.diff" || true
|
|
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg.diff" \
|
|
>"$artifacts/guest-dmesg-added.txt"
|
|
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
|
|
"$artifacts/guest-dmesg-added.txt"; then
|
|
pre15_dut_fail 'TC168 produced panic, WITNESS, or UAF evidence'
|
|
fi
|
|
printf '%s\n' \
|
|
'TC168-cache-inflight B33 QEMU PASS' \
|
|
'Exact KLD ownership, FBT-proven cache init/read calls, full-extent success/failure waiters, unmount drain, and cleanup exercised' \
|
|
'TC130 and full feature suite NOT_RUN'
|