This commit is contained in:
2026-08-18 09:20:44 +02:00
commit b826cd721a
522 changed files with 93730 additions and 0 deletions
+667
View File
@@ -0,0 +1,667 @@
#!/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"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B28-partial.json
generator=$fixture_dir/B28-partial-fixtures.py
probe=$fixture_dir/B28-partial-probe.c
kld_builder=$fixture_dir/B28-build-kld.sh
gate_script=$PRE15_DUT/tests/pre15/gates/P15-086.sh
gate_input=$PRE15_DUT/tests/pre15/gates/P15-086-input.json
gate_result=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/result.json
gate_capabilities=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/authorized-capabilities.json
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/fixtures-first
second=$PRE15_CASE_TMP/fixtures-second
action=${1:-runtime}
case "$action" in
runtime|k2) ;;
*) pre15_fail_usage "unknown B28 action: $action" ;;
esac
if test "$action" = k2; then
for tool in awk clang cmp diff file nm sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B28 K2 tool: $tool"
done
pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
pre15_record_fixture b28-kld-builder "$kld_builder"
for source in compress.h decompressor.c decompressor_lz4.c \
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
for config in 0 1; do
module=$PRE15_CASE_TMP/B28-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/B28-build-zstdio$config.stdout" \
2>"$artifacts/B28-build-zstdio$config.stderr"; then
pre15_dut_fail "B28 zstdio$config cross-target KLD build failed"
fi
file "$module" >"$artifacts/B28-zstdio$config-file.txt"
sha256sum "$module" >"$artifacts/B28-zstdio$config-sha256.txt"
nm -g "$module" | LC_ALL=C sort \
>"$artifacts/B28-zstdio$config-nm-global.txt"
nm -u "$module" | LC_ALL=C sort \
>"$artifacts/B28-zstdio$config-nm-u.txt"
done
awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio0-nm-global.txt" \
>"$PRE15_CASE_TMP/B28-zstdio0-global-names.txt"
awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio1-nm-global.txt" \
>"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt"
if ! diff -u "$PRE15_CASE_TMP/B28-zstdio0-global-names.txt" \
"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt" \
>"$artifacts/B28-config-nm-global.diff"; then
pre15_dut_fail 'B28 K2 global symbols differ by ZSTDIO configuration'
fi
if ! grep -q ' z_erofs_decompress_supports_subextent$' \
"$artifacts/B28-zstdio0-nm-global.txt"; then
pre15_dut_fail 'B28 K2 partial capability symbol is absent'
fi
pre15_target_reached
printf '%s\n' \
'B28 targeted FreeBSD 15 K2 PASS' \
'zstdio0/zstdio1 link, defined-global parity, optional Zstd ABI, and partial capability symbol PASS' \
'KLD and object files removed by owned case cleanup'
exit 0
fi
for tool in cc cmp diff file fsck.erofs git mkfs.erofs nm python3 sha256sum tar timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B28 host tool: $tool"
done
pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
pre15_record_fixture b28-spec "$spec"
pre15_record_fixture b28-generator "$generator"
pre15_record_fixture b28-probe "$probe"
pre15_record_fixture b28-kld-builder "$kld_builder"
pre15_record_fixture b28-gate "$gate_script"
pre15_record_fixture b28-gate-input "$gate_input"
pre15_record_fixture b28-gate-result "$gate_result"
pre15_record_fixture b28-gate-capabilities "$gate_capabilities"
for source in compress.h decompressor.c decompressor_lz4.c \
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
--output "$first" >"$artifacts/generate-first.stdout" \
2>"$artifacts/generate-first.stderr"; then
pre15_runner_fail 'B28 first real EROFS fixture generation failed'
fi
if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
--output "$second" >"$artifacts/generate-second.stdout" \
2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B28 second real EROFS fixture generation failed'
fi
if ! diff -ru "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
pre15_runner_fail 'B28 real EROFS fixtures are not byte reproducible'
fi
cp "$first/fixture-index.json" "$artifacts/B28-fixture-index.json"
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
pre15_target_reached
else
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" "$gate_result" \
"$gate_capabilities" "$artifacts/B28-source-audit.json" \
"$PRE15_CASE_TMP/decision-harness.c" <<'PY'
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
spec_path = Path(sys.argv[3])
gate_result_path = Path(sys.argv[4])
gate_capabilities_path = Path(sys.argv[5])
report_path = Path(sys.argv[6])
harness_path = Path(sys.argv[7])
spec = json.loads(spec_path.read_text(encoding="ascii"))
baseline = spec["baseline"]
implementation = "7a0d7e4a4047ac6d6130a3be5cb677cb94979fe7"
runtime_base = "d09924362eb29819bd393e1e86602eb38c7608c6"
def fail(message: str) -> None:
raise SystemExit(message)
def sha256_path(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def committed(commit: str, name: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{commit}:repo-pre-15/src/{name}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if completed.returncode != 0:
fail(f"cannot read B28 source {commit}:{name}: {completed.stderr.strip()}")
return completed.stdout
def function(source: str, name: str) -> str:
marker = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if marker is None:
fail(f"missing function: {name}")
name_line = source.rfind("\n", 0, marker.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", marker.end())
if brace < 0:
fail(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
fail(f"unterminated function: {name}")
if (
spec.get("schema") != 1
or spec.get("batch") != "B28"
or spec.get("candidate") != "P15-086"
or spec.get("test") != "TC176-stream-runtime"
):
fail("B28 fixture identity changed")
for name, commit in (
("baseline", baseline),
("implementation", implementation),
("runtime base", runtime_base),
):
if subprocess.check_output(
["git", "-C", str(root), "rev-parse", commit], text=True
).strip() != commit:
fail(f"B28 {name} identity changed")
gate = json.loads(gate_result_path.read_text(encoding="ascii"))
capabilities = json.loads(gate_capabilities_path.read_text(encoding="ascii"))
if (
gate.get("status") != "GO"
or gate.get("g04") != "GO"
or gate.get("g05") != "GO"
or gate.get("b28") != "AUTHORIZED"
or gate.get("resolved_base") != "6bf5724619be70f805bbe7d1ba77dd70cdced69f"
):
fail("frozen P15-086 gate no longer authorizes B28")
if capabilities != {
"full_fallback": ["zstd"],
"partial": ["deflate", "lz4", "lzma"],
"persistent_state_added": False,
"schema": 1,
}:
fail("P15-086 per-codec authorization changed")
for name, expected in (
("gate script", spec["gate"]["script_sha256"]),
("gate input", spec["gate"]["input_sha256"]),
("gate result", spec["gate"]["result_sha256"]),
):
path = {
"gate script": dut / "tests/pre15/gates/P15-086.sh",
"gate input": dut / "tests/pre15/gates/P15-086-input.json",
"gate result": gate_result_path,
}[name]
if sha256_path(path) != expected:
fail(f"{name} hash changed")
source_names = (
"compress.h",
"decompressor.c",
"decompressor_lz4.c",
"decompressor_lzma.c",
"decompressor_deflate.c",
"decompressor_zstd.c",
"zdata.c",
)
baseline_sources = {name: committed(baseline, name) for name in source_names}
implemented_sources = {name: committed(implementation, name) for name in source_names}
sources = {
name: (dut / "src" / name).read_text(encoding="utf-8")
for name in source_names
}
if implemented_sources["decompressor_lz4.c"] != baseline_sources["decompressor_lz4.c"]:
fail("B28 changed the already-capable LZ4 backend implementation")
if "bool supports_subextent;" not in implemented_sources["compress.h"]:
fail("B28 descriptor capability is absent")
if implemented_sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
fail("B28 capability prototype count changed")
if "bool supports_subextent;" not in sources["compress.h"]:
fail("current tree lost the B28 descriptor capability")
if sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
fail("current B28 capability prototype count changed")
def read_descriptor_decisions(source_set: dict[str, str]) -> dict[str, bool]:
decisions = {}
for codec in ("lzma", "deflate", "zstd"):
value = re.search(
rf"const struct z_erofs_decompressor z_erofs_{codec}_decomp = \{{.*?\.supports_subextent = ([01]),",
source_set[f"decompressor_{codec}.c"],
re.DOTALL,
)
if value is None:
fail(f"missing {codec} descriptor capability")
decisions[codec] = value.group(1) == "1"
lz4 = re.search(
r"static const struct z_erofs_decompressor z_erofs_lz4_decomp = \{.*?\.supports_subextent = ([01]),",
source_set["decompressor.c"],
re.DOTALL,
)
if lz4 is None:
fail("missing LZ4 descriptor capability")
decisions["lz4"] = lz4.group(1) == "1"
return decisions
implemented_decisions = read_descriptor_decisions(implemented_sources)
if implemented_decisions != {
"lz4": True, "lzma": True, "deflate": True, "zstd": False
}:
fail(f"B28 implementation decisions differ from G04/G05: {implemented_decisions}")
descriptor_decisions = read_descriptor_decisions(sources)
if descriptor_decisions != {
"lz4": True, "lzma": True, "deflate": True, "zstd": False
}:
fail(f"current partial descriptor decisions changed: {descriptor_decisions}")
expected_zstd = committed(runtime_base, "decompressor_zstd.c")
if function(sources["decompressor_zstd.c"], "z_erofs_zstd_finish") != \
function(expected_zstd, "z_erofs_zstd_finish"):
fail("current Zstd completion policy differs from the accepted transform")
unchanged_zdata = (
"z_erofs_extent_cache_match",
"z_erofs_extent_cache_copy",
"z_erofs_extent_cache_publish",
"z_erofs_extent_cache_init",
"z_erofs_extent_cache_fini",
"z_erofs_extent_cache_eligible",
"z_erofs_read_data",
"z_erofs_read_uio",
)
baseline_zdata = baseline_sources["zdata.c"]
for name in unchanged_zdata:
if function(implemented_sources["zdata.c"], name) != function(baseline_zdata, name):
fail(f"B28 changed unrelated zdata function {name}")
implemented_decode_length = function(implemented_sources["zdata.c"], "z_erofs_decode_length")
implemented_read_extent = function(implemented_sources["zdata.c"], "z_erofs_read_extent")
implemented_do_read = function(implemented_sources["zdata.c"], "z_erofs_do_read")
decode_length = function(sources["zdata.c"], "z_erofs_decode_length")
decode_extent = function(sources["zdata.c"], "z_erofs_decode_extent")
do_read = function(sources["zdata.c"], "z_erofs_do_read")
if decode_length != implemented_decode_length:
fail("current tree changed the B28 decode-length transform")
if decode_extent[decode_extent.index("{") :] != implemented_read_extent[
implemented_read_extent.index("{") :
]:
fail("current tree changed B28 provider or decoded-buffer ownership")
for anchor in (
"z_erofs_decompress_supports_subextent(map)",
"*partial = (map->m_flags & EROFS_MAP_PARTIAL_REF) != 0;",
"*decoded_len = end;",
"*partial = true;",
):
if anchor not in implemented_decode_length:
fail(f"B28 decode-length anchor missing: {anchor}")
for anchor in (
"erofs_read_metadata(",
"erofs_read_physical(",
"erofs_put_metabuf(&buf);",
"erofs_brelse(compressed);",
"free(decoded, M_EROFS);",
"*bufp = decoded;",
):
if implemented_read_extent.count(anchor) != function(baseline_zdata, "z_erofs_read_extent").count(anchor):
fail(f"B28 changed provider or cleanup count: {anchor}")
if not (
implemented_do_read.index("z_erofs_extent_cache_copy")
< implemented_do_read.index("z_erofs_decode_length")
< implemented_do_read.index("z_erofs_read_extent")
< implemented_do_read.index("cache_eligible && !partial")
< implemented_do_read.index("z_erofs_extent_cache_publish")
):
fail("B28 cache/decode/publication order changed")
if "partial, &decoded" not in implemented_do_read:
fail("B28 does not pass the selected partial mode to extent decode")
if not (
do_read.index("z_erofs_decode_length")
< do_read.index("cache_eligible = !partial")
< do_read.index("z_erofs_decode_extent")
):
fail("current tree no longer excludes partial decodes from cache admission")
if "partial, &decoded" not in do_read:
fail("current tree does not pass B28 partial mode to extent decode")
changed = set(
subprocess.check_output(
[
"git",
"-C",
str(root),
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
implementation,
],
text=True,
).splitlines()
)
expected_write_set = {
"repo-pre-15/src/compress.h",
"repo-pre-15/src/decompressor.c",
"repo-pre-15/src/decompressor_deflate.c",
"repo-pre-15/src/decompressor_lzma.c",
"repo-pre-15/src/decompressor_zstd.c",
"repo-pre-15/src/zdata.c",
"repo-pre-15/tests/pre15/cases/B28-partial.sh",
"repo-pre-15/tests/pre15/fixtures/B28-build-kld.sh",
"repo-pre-15/tests/pre15/fixtures/B28-partial-fixtures.py",
"repo-pre-15/tests/pre15/fixtures/B28-partial-probe.c",
"repo-pre-15/tests/pre15/fixtures/B28-partial.json",
}
if changed != expected_write_set:
fail(f"B28 exact write set differs: {sorted(changed ^ expected_write_set)}")
if any(re.search(r"return\s*\(\s*-E[A-Z0-9_]+", source) for source in sources.values()):
fail("negative Linux errno entered B28")
harness = r'''#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#define EINTEGRITY 97
#define EOVERFLOW 84
#define EROFS_MAP_PARTIAL_REF 0x20
typedef uint64_t erofs_off_t;
struct erofs_map_blocks {
uint64_t m_llen;
unsigned int m_flags;
unsigned int m_algorithmformat;
};
static bool z_erofs_decompress_supports_subextent(const struct erofs_map_blocks *map)
{
return map->m_algorithmformat <= 2;
}
''' + decode_length + r'''
static int failures;
static void check(const char *name, struct erofs_map_blocks map, uint64_t off,
size_t want, int expected_error, size_t expected_length, bool expected_partial)
{
size_t length = 0;
bool partial = false;
int error = z_erofs_decode_length(&map, off, want, &length, &partial);
if (error != expected_error || (!error &&
(length != expected_length || partial != expected_partial))) {
fprintf(stderr, "%s error=%d length=%zu partial=%d\n", name,
error, length, partial);
failures++;
}
}
int main(void)
{
check("lz4 prefix", (struct erofs_map_blocks){1038906, 0, 0}, 0,
4096, 0, 4096, true);
check("lzma middle", (struct erofs_map_blocks){151552, 0, 1}, 8192,
4096, 0, 12288, true);
check("deflate cross", (struct erofs_map_blocks){22878, 0, 2}, 3584,
4096, 0, 7680, true);
check("zstd prefix", (struct erofs_map_blocks){151552, 0, 3}, 0,
4096, 0, 151552, false);
check("lzma tail", (struct erofs_map_blocks){151552, 0, 1}, 147456,
4096, 0, 151552, false);
check("zstd partial ref", (struct erofs_map_blocks){151552,
EROFS_MAP_PARTIAL_REF, 3}, 0, 4096, 0, 4096, true);
check("shifted fallback", (struct erofs_map_blocks){65536, 0, 4}, 0,
4096, 0, 65536, false);
check("bad range", (struct erofs_map_blocks){4096, 0, 0}, 4090,
16, EINTEGRITY, 0, false);
return failures != 0;
}
'''
harness_path.write_text(harness, encoding="ascii")
report = {
"baseline": baseline,
"batch": "B28",
"cache_hit_before_decode": True,
"candidate": "P15-086",
"descriptor_decisions": descriptor_decisions,
"frozen_gate_full_fallback": ["zstd"],
"current_full_fallback": ["shifted", "interlaced", "unknown", "zstd"],
"current_partial": ["deflate", "lz4", "lzma"],
"gate": "GO",
"implementation": implementation,
"partial_cache_publication": False,
"partial_reference_preserved": True,
"persistent_state_added": False,
"positive_errno": True,
"provider_and_cleanup_unchanged": True,
"runtime_base": runtime_base,
"status": "PASS",
"test": "TC176-stream-runtime",
"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 'B28 gate/source/write-set audit failed'
fi
if ! cc -std=c11 -Wall -Wextra -Werror "$PRE15_CASE_TMP/decision-harness.c" \
-o "$PRE15_CASE_TMP/decision-harness" \
>"$artifacts/decision-harness-build.stdout" \
2>"$artifacts/decision-harness-build.stderr" || \
! "$PRE15_CASE_TMP/decision-harness" \
>"$artifacts/decision-harness.stdout" \
2>"$artifacts/decision-harness.stderr"; then
pre15_dut_fail 'B28 extracted decode-length harness failed'
fi
cp "$gate_result" "$artifacts/P15-086-result.json"
cp "$gate_capabilities" "$artifacts/P15-086-capabilities.json"
sha256sum "$artifacts/P15-086-result.json" \
"$artifacts/P15-086-capabilities.json" \
"$artifacts/B28-fixture-index.json" \
"$artifacts/B28-source-audit.json" >"$artifacts/SHA256SUMS"
fi
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC176 B28 host partial-subextent subset PASS' \
'B28 LZ4/LZMA/Deflate partial policy and Zstd full fallback, cache exclusion, arithmetic, real fixtures, and cleanup PASS' \
'QEMU runtime NOT_RUN in host mode' \
'Full feature suite NOT_RUN'
exit 0
fi
for tool in awk clang scp; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B28 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}"
fixture_archive=$PRE15_CASE_TMP/B28-fixtures.tar.gz
module=$PRE15_CASE_TMP/B28-erofs-zstdio1.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" 1 >"$artifacts/B28-kld-build.stdout" \
2>"$artifacts/B28-kld-build.stderr"; then
pre15_dut_fail 'B28 cross-target zstdio1 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B28-kld-file.txt"
sha256sum "$module" >"$artifacts/B28-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B28-kld-nm-u.txt"
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/B28-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B28-partial-probe.c || \
! pre15_scp "$module" /root/B28-erofs-zstdio1.ko; then
pre15_infra_blocked 'could not transfer B28 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/B28-fixtures && mkdir /root/B28-fixtures && tar -xzf /root/B28-fixtures.tar.gz -C /root/B28-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B28-partial-probe /root/B28-partial-probe.c'; then
pre15_infra_blocked 'could not prepare B28 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
if ! pre15_guest_ssh_bounded kldload /root/B28-erofs-zstdio1.ko \
>"$artifacts/B28-kldload.stdout" 2>"$artifacts/B28-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' \
"$artifacts/B28-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B28-kldload.stderr"; then
pre15_infra_blocked 'guest kernel lacks the planned ZSTDIO provider ABI'
fi
pre15_dut_fail 'B28 exact-source zstdio1 KLD failed to load'
fi
pre15_own_guest_kld B28-erofs-zstdio1.ko 'B28 exact-source KLD'
attach_mount()
{
image=$1
label=$2
B28_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/B28-fixtures/images/$image") || \
pre15_dut_fail "$label md attach failed"
case "$B28_MD" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected B28 md unit: $B28_MD" ;;
esac
pre15_own_guest_md "$B28_MD" "$label md"
B28_MOUNT=/mnt/pre15-b28-$label
pre15_guest_ssh_bounded mkdir -p "$B28_MOUNT"
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B28_MD" "$B28_MOUNT" || \
pre15_dut_fail "$label mount failed"
pre15_own_guest_mount "$B28_MOUNT" "$label mount"
}
valid_assertions=0
corrupt_range_assertions=0
full_corruption_assertions=0
while IFS='|' read -r codec source logical_offset logical_length decision; do
reference=/root/B28-fixtures/sources/$source/payload.bin
attach_mount "$codec-valid.erofs" "$codec-valid"
for range in '0 4096' '3584 4096' '8192 4096'; do
set -- $range
offset=$((logical_offset + $1))
pre15_guest_ssh_bounded /root/B28-partial-probe range \
"$B28_MOUNT/payload.bin" "$reference" "$offset" "$2" || \
pre15_dut_fail "$codec valid subextent mismatch at $offset"
valid_assertions=$((valid_assertions + 1))
done
tail_offset=$((logical_offset + logical_length - 4096))
pre15_guest_ssh_bounded /root/B28-partial-probe range \
"$B28_MOUNT/payload.bin" "$reference" "$tail_offset" 4096 || \
pre15_dut_fail "$codec valid tail fallback mismatch"
valid_assertions=$((valid_assertions + 1))
attach_mount "$codec-corrupt.erofs" "$codec-corrupt"
if test "$decision" = GO; then
pre15_guest_ssh_bounded /root/B28-partial-probe range \
"$B28_MOUNT/payload.bin" "$reference" "$logical_offset" 4096 || \
pre15_dut_fail "$codec range-before-corruption partial decode failed"
else
pre15_guest_ssh_bounded /root/B28-partial-probe range-errno \
"$B28_MOUNT/payload.bin" "$logical_offset" 4096 97 || \
pre15_dut_fail "$codec did not use exact full fallback"
fi
corrupt_range_assertions=$((corrupt_range_assertions + 1))
pre15_guest_ssh_bounded /root/B28-partial-probe full-errno \
"$B28_MOUNT/payload.bin" 97 || \
pre15_dut_fail "$codec full corruption did not return EINTEGRITY"
full_corruption_assertions=$((full_corruption_assertions + 1))
done <<'EOF'
deflate|stream|114326|22878|GO
lz4|lz4|0|1038906|GO
lzma|stream|0|151552|GO
zstd|stream|0|151552|FULL_FALLBACK
EOF
if test "$valid_assertions" -ne 16 || \
test "$corrupt_range_assertions" -ne 4 || \
test "$full_corruption_assertions" -ne 4; then
pre15_runner_fail "B28 assertion count mismatch: valid=$valid_assertions corrupt-range=$corrupt_range_assertions full-corruption=$full_corruption_assertions"
fi
pre15_guest_ssh_bounded dmesg >"$artifacts/B28-dmesg.txt"
printf '%s\n' \
'TC176 B28 QEMU partial-subextent subset PASS' \
'valid-ranges=16 partial-before-corruption=4 full-corruption=4 errno=97' \
'Full feature suite NOT_RUN'